@ninkch/dsh-infinite-canvas 0.19.4 → 0.19.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +4 -4
  2. package/README.zh-CN.md +6 -6
  3. package/assets/canvas-director/SKILL.md +14 -14
  4. package/assets/canvas-director/forms/interview.md +1 -1
  5. package/assets/canvas-director/manifest.json +8 -8
  6. package/assets/canvas-director/pack.js +8 -12
  7. package/assets/canvas-studio/SKILL.md +2 -2
  8. package/assets/director/clips/dodge_back.json +1 -0
  9. package/assets/director/clips/idle_02.json +1 -0
  10. package/assets/director/clips/idle_03.json +1 -0
  11. package/assets/director/clips/jump.json +1 -0
  12. package/assets/director/clips/manifest.json +156 -0
  13. package/assets/director/clips/run.json +1 -0
  14. package/assets/director/clips/run_back.json +1 -0
  15. package/assets/director/clips/run_forward.json +1 -0
  16. package/assets/director/clips/run_left.json +1 -0
  17. package/assets/director/clips/run_right.json +1 -0
  18. package/assets/director/clips/run_stop.json +1 -0
  19. package/assets/director/clips/running_jump.json +1 -0
  20. package/assets/director/clips/sit_laugh.json +1 -0
  21. package/assets/director/clips/turn_left.json +1 -0
  22. package/assets/director/clips/turn_right.json +1 -0
  23. package/assets/director/clips/walk.json +1 -0
  24. package/assets/director/clips/walk_back.json +1 -0
  25. package/assets/director/clips/walk_forward.json +1 -0
  26. package/assets/director/clips/walk_left.json +1 -0
  27. package/assets/director/clips/walk_right.json +1 -0
  28. package/assets/director/engine.js +291 -65
  29. package/assets/director/models/explorer.json +1 -0
  30. package/assets/director/models/index.json +10 -0
  31. package/assets/director/models/soldier.json +1 -1
  32. package/assets/director/models/xbot.json +1 -1
  33. package/client.js +19727 -19577
  34. package/docs/guide.en.md +5 -5
  35. package/docs/guide.zh-CN.md +9 -9
  36. package/index-core-skill-profile.js +2 -6
  37. package/index.js +10 -25
  38. package/package.json +5 -1
  39. package/skill-pack.js +19 -14
@@ -1,4 +1,4 @@
1
- export const ENGINE_VERSION = 'ground-off-1';
1
+ export const ENGINE_VERSION = 'explorer-2'; // 改引擎必递增:client 靠它提示用户刷新页面 // 改引擎必递增:client 靠它提示用户刷新页面
2
2
  /**
3
3
  * 3D 导演台引擎(独立 ES 模块,零依赖)。
4
4
  *
@@ -786,6 +786,195 @@ export const BONE_POSES = [
786
786
  { bone: 'LeftArm', to: [0.78, 0.22, 0.58] }, { bone: 'RightArm', to: [-0.80, 0.18, 0.56] },
787
787
  { bone: 'LeftForeArm', to: [0.62, 0.06, 0.78] }, { bone: 'RightForeArm', to: [-0.64, 0.02, 0.76] },
788
788
  ] },
789
+ // ── 导演姿势库(复刻 awplanet DIRECTOR_POSE_RECIPES,19 套)──
790
+ { id: 'ap_stand_relaxed', zh: '站姿 · 放松', en: 'Stand relaxed', rootY: 0,
791
+ // 来源:awplanet DIRECTOR_POSE_RECIPES(加法欧拉)→ 本表 base 四元数(同语义)
792
+ base: {
793
+ Spine: [-0.08, 0.02, -0.05],
794
+ LeftArm: [0.16, 0.02, 0.18],
795
+ RightArm: [0.12, -0.02, -0.16],
796
+ LeftUpLeg: [0.03, 0.04, 0.08],
797
+ RightUpLeg: [-0.02, -0.02, -0.05]
798
+ } },
799
+ { id: 'ap_stand_contrapposto', zh: '站姿 · 重心对立', en: 'Stand contrapposto', rootY: 0,
800
+ // 来源:awplanet DIRECTOR_POSE_RECIPES(加法欧拉)→ 本表 base 四元数(同语义)
801
+ base: {
802
+ Spine: [-0.05, 0.05, -0.16],
803
+ Spine1: [0.03, -0.04, 0.12],
804
+ Spine2: [0.03, -0.04, 0.12],
805
+ LeftUpLeg: [0.02, 0.1, 0.16],
806
+ RightUpLeg: [-0.04, -0.04, -0.11],
807
+ LeftArm: [0.08, -0.06, 0.34],
808
+ RightArm: [0.04, 0.04, -0.22]
809
+ } },
810
+ { id: 'ap_stand_alert', zh: '站姿 · 警戒', en: 'Stand alert', rootY: 0,
811
+ // 来源:awplanet DIRECTOR_POSE_RECIPES(加法欧拉)→ 本表 base 四元数(同语义)
812
+ base: {
813
+ Spine: [0.08, 0, 0],
814
+ Spine1: [0.08, 0, 0],
815
+ Neck: [0.06, 0, 0],
816
+ LeftArm: [-0.08, 0.02, 0.08],
817
+ RightArm: [-0.08, -0.02, -0.08],
818
+ LeftUpLeg: [-0.03, 0, 0.04],
819
+ RightUpLeg: [-0.03, 0, -0.04]
820
+ } },
821
+ { id: 'ap_stand_lean', zh: '站姿 · 倚靠', en: 'Stand lean', rootY: 0,
822
+ // 来源:awplanet DIRECTOR_POSE_RECIPES(加法欧拉)→ 本表 base 四元数(同语义)
823
+ base: {
824
+ Spine: [-0.16, 0.08, -0.2],
825
+ Spine1: [0.05, -0.04, 0.13],
826
+ Spine2: [0.05, -0.04, 0.13],
827
+ LeftUpLeg: [0.1, 0.05, 0.1],
828
+ LeftArm: [0.22, -0.1, 0.38],
829
+ RightArm: [0.05, 0.08, -0.18]
830
+ } },
831
+ { id: 'ap_crossed_arms', zh: '站姿 · 抱臂', en: 'Crossed arms', rootY: 0,
832
+ // 来源:awplanet DIRECTOR_POSE_RECIPES(加法欧拉)→ 本表 base 四元数(同语义)
833
+ base: {
834
+ Spine: [-0.04, 0, 0.02],
835
+ LeftArm: [0.72, 0.18, 0.78],
836
+ RightArm: [0.72, -0.18, -0.78],
837
+ LeftForeArm: [0.34, -0.1, -1.18],
838
+ RightForeArm: [0.34, 0.1, 1.18]
839
+ } },
840
+ { id: 'ap_hands_hips', zh: '站姿 · 叉腰', en: 'Hands on hips', rootY: 0,
841
+ // 来源:awplanet DIRECTOR_POSE_RECIPES(加法欧拉)→ 本表 base 四元数(同语义)
842
+ base: {
843
+ Spine: [0.02, 0, -0.04],
844
+ LeftArm: [0.48, -0.1, 0.68],
845
+ RightArm: [0.48, 0.1, -0.68],
846
+ LeftForeArm: [0.16, -0.2, -0.92],
847
+ RightForeArm: [0.16, 0.2, 0.92]
848
+ } },
849
+ { id: 'ap_sit_low', zh: '坐 · 低坐', en: 'Sit low', rootY: -0.52,
850
+ // 来源:awplanet DIRECTOR_POSE_RECIPES(加法欧拉)→ 本表 base 四元数(同语义)
851
+ base: {
852
+ Spine: [-0.12, 0, 0],
853
+ LeftUpLeg: [-1.18, 0.06, 0.12],
854
+ RightUpLeg: [-1.18, -0.06, -0.12],
855
+ LeftLeg: [1.18, 0, 0],
856
+ RightLeg: [1.18, 0, 0],
857
+ LeftArm: [0.32, 0.04, 0.18],
858
+ RightArm: [0.32, -0.04, -0.18]
859
+ } },
860
+ { id: 'ap_sit_side', zh: '坐 · 侧坐', en: 'Sit side', rootY: -0.52,
861
+ // 来源:awplanet DIRECTOR_POSE_RECIPES(加法欧拉)→ 本表 base 四元数(同语义)
862
+ base: {
863
+ Spine: [-0.08, 0.14, -0.1],
864
+ LeftUpLeg: [-1, 0.2, 0.46],
865
+ RightUpLeg: [-1.18, -0.26, 0.18],
866
+ LeftLeg: [1.02, 0, -0.12],
867
+ RightLeg: [1.08, 0, 0.18]
868
+ } },
869
+ { id: 'ap_crouch', zh: '蹲 · 预备', en: 'Crouch ready', rootY: -0.28,
870
+ // 来源:awplanet DIRECTOR_POSE_RECIPES(加法欧拉)→ 本表 base 四元数(同语义)
871
+ base: {
872
+ Spine: [-0.32, 0, 0],
873
+ LeftUpLeg: [-0.92, 0.04, 0.12],
874
+ RightUpLeg: [-0.92, -0.04, -0.12],
875
+ LeftLeg: [1.32, 0, 0],
876
+ RightLeg: [1.32, 0, 0],
877
+ LeftArm: [0.55, -0.08, 0.25],
878
+ RightArm: [0.55, 0.08, -0.25]
879
+ } },
880
+ { id: 'ap_kneel', zh: '跪 · 单膝', en: 'Kneel one knee', rootY: -0.42,
881
+ // 来源:awplanet DIRECTOR_POSE_RECIPES(加法欧拉)→ 本表 base 四元数(同语义)
882
+ base: {
883
+ Spine: [-0.18, 0, 0],
884
+ LeftUpLeg: [-1.22, 0.06, 0.12],
885
+ LeftLeg: [1.52, 0, 0],
886
+ RightUpLeg: [-0.35, -0.08, -0.12],
887
+ RightLeg: [0.72, 0, 0],
888
+ LeftArm: [0.35, -0.05, 0.18],
889
+ RightArm: [0.35, 0.05, -0.18]
890
+ } },
891
+ { id: 'ap_kneel_both', zh: '跪 · 双膝', en: 'Kneel both knees', rootY: -0.55,
892
+ // 来源:awplanet DIRECTOR_POSE_RECIPES(加法欧拉)→ 本表 base 四元数(同语义)
893
+ base: {
894
+ Spine: [-0.1, 0, 0],
895
+ LeftUpLeg: [-1.28, 0.08, 0.12],
896
+ RightUpLeg: [-1.28, -0.08, -0.12],
897
+ LeftLeg: [1.48, 0, 0],
898
+ RightLeg: [1.48, 0, 0],
899
+ LeftArm: [0.22, -0.04, 0.14],
900
+ RightArm: [0.22, 0.04, -0.14]
901
+ } },
902
+ { id: 'ap_lie_prone', zh: '卧 · 俯卧', en: 'Lie prone', rootY: -0.82,
903
+ // 来源:awplanet DIRECTOR_POSE_RECIPES(加法欧拉)→ 本表 base 四元数(同语义)
904
+ base: {
905
+ Spine: [-1.32, 0, 0],
906
+ Spine1: [0.1, 0, 0],
907
+ Spine2: [0.1, 0, 0],
908
+ LeftUpLeg: [0.12, 0.04, 0.1],
909
+ RightUpLeg: [0.12, -0.04, -0.1],
910
+ LeftArm: [0.92, -0.1, 0.38],
911
+ RightArm: [0.92, 0.1, -0.38]
912
+ } },
913
+ { id: 'ap_lie_back', zh: '卧 · 仰卧', en: 'Lie back', rootY: -0.82,
914
+ // 来源:awplanet DIRECTOR_POSE_RECIPES(加法欧拉)→ 本表 base 四元数(同语义)
915
+ base: {
916
+ Spine: [1.28, 0, 0],
917
+ LeftUpLeg: [-0.16, 0.08, 0.12],
918
+ RightUpLeg: [-0.16, -0.08, -0.12],
919
+ LeftLeg: [0.42, 0, 0],
920
+ RightLeg: [0.42, 0, 0],
921
+ LeftArm: [0.7, -0.2, 0.62],
922
+ RightArm: [0.7, 0.2, -0.62]
923
+ } },
924
+ { id: 'ap_point_forward', zh: '指向前方', en: 'Point forward', rootY: 0,
925
+ // 来源:awplanet DIRECTOR_POSE_RECIPES(加法欧拉)→ 本表 base 四元数(同语义)
926
+ base: {
927
+ Spine: [0.03, -0.1, 0],
928
+ RightArm: [-0.12, -0.52, -1.18],
929
+ RightForeArm: [-0.08, -0.12, -0.28],
930
+ LeftArm: [0.18, 0.06, 0.18]
931
+ } },
932
+ { id: 'ap_reach_up', zh: '举手够物', en: 'Reach up', rootY: 0,
933
+ // 来源:awplanet DIRECTOR_POSE_RECIPES(加法欧拉)→ 本表 base 四元数(同语义)
934
+ base: {
935
+ Spine: [0.08, 0, 0.06],
936
+ RightArm: [-1.42, -0.14, -0.36],
937
+ RightForeArm: [-0.18, 0.06, -0.08],
938
+ LeftArm: [0.2, 0.02, 0.22]
939
+ } },
940
+ { id: 'ap_guard_ready', zh: '戒备势', en: 'Guard ready', rootY: 0,
941
+ // 来源:awplanet DIRECTOR_POSE_RECIPES(加法欧拉)→ 本表 base 四元数(同语义)
942
+ base: {
943
+ Spine: [-0.16, 0, 0],
944
+ LeftArm: [0.76, -0.08, 0.42],
945
+ RightArm: [0.76, 0.08, -0.42],
946
+ LeftForeArm: [0.42, -0.18, -0.76],
947
+ RightForeArm: [0.42, 0.18, 0.76],
948
+ LeftUpLeg: [-0.12, 0.06, 0.1],
949
+ RightUpLeg: [-0.12, -0.06, -0.1]
950
+ } },
951
+ { id: 'ap_surrender', zh: '投降势', en: 'Surrender', rootY: 0,
952
+ // 来源:awplanet DIRECTOR_POSE_RECIPES(加法欧拉)→ 本表 base 四元数(同语义)
953
+ base: {
954
+ Spine: [0.04, 0, 0],
955
+ LeftArm: [-1.28, 0.1, 0.46],
956
+ RightArm: [-1.28, -0.1, -0.46],
957
+ LeftForeArm: [-0.24, 0.05, -0.22],
958
+ RightForeArm: [-0.24, -0.05, 0.22]
959
+ } },
960
+ { id: 'ap_hands_head', zh: '双手抱头', en: 'Hands on head', rootY: 0,
961
+ // 来源:awplanet DIRECTOR_POSE_RECIPES(加法欧拉)→ 本表 base 四元数(同语义)
962
+ base: {
963
+ Spine: [-0.08, 0, 0],
964
+ LeftArm: [-0.84, 0.18, 0.82],
965
+ RightArm: [-0.84, -0.18, -0.82],
966
+ LeftForeArm: [-0.54, 0, -1.12],
967
+ RightForeArm: [-0.54, 0, 1.12]
968
+ } },
969
+ { id: 'ap_talk_open', zh: '说话 · 张开', en: 'Talk open', rootY: 0,
970
+ // 来源:awplanet DIRECTOR_POSE_RECIPES(加法欧拉)→ 本表 base 四元数(同语义)
971
+ base: {
972
+ Spine: [0.02, 0.06, -0.04],
973
+ LeftArm: [0.1, -0.16, 0.58],
974
+ RightArm: [0.1, 0.16, -0.58],
975
+ LeftForeArm: [0.34, -0.12, -0.34],
976
+ RightForeArm: [0.34, 0.12, 0.34]
977
+ } },
789
978
  ];
790
979
 
791
980
 
@@ -1434,18 +1623,18 @@ export function decodeActionClips(list) {
1434
1623
  */
1435
1624
  // waist:腰部横向系数(男性 >1 直腰方厚,女性 <1 收腰);chestFlat:男性压扁胸部正面
1436
1625
  const BODY_SHAPE_OF = {
1437
- m_standard: { bust: 0, belly: 0.00, muscle: 0.15, shoulder: 1.08, hip: 1.02, waist: 1.08, chestFlat: 0.10 },
1438
- f_standard: { bust: 1.00, belly: 0.04, muscle: 0.00, shoulder: 0.94, hip: 1.10, waist: 0.88, chestFlat: 0 },
1439
- m_teen: { bust: 0, belly: 0.02, muscle: 0.06, shoulder: 0.98, hip: 0.98, waist: 1.04, chestFlat: 0.08 },
1440
- f_teen: { bust: 0.62, belly: 0.03, muscle: 0.00, shoulder: 0.90, hip: 1.02, waist: 0.90, chestFlat: 0 },
1441
- m_child: { bust: 0, belly: 0.06, muscle: 0.00, shoulder: 0.84, hip: 0.88, waist: 1.02, chestFlat: 0.04 },
1442
- f_child: { bust: 0.18, belly: 0.06, muscle: 0.00, shoulder: 0.82, hip: 0.90, waist: 0.94, chestFlat: 0 },
1443
- m_slim: { bust: 0, belly: -0.02, muscle: 0.05, shoulder: 1.02, hip: 0.96, waist: 1.04, chestFlat: 0.10 },
1444
- f_slim: { bust: 0.78, belly: -0.03, muscle: 0.00, shoulder: 0.88, hip: 0.98, waist: 0.84, chestFlat: 0 },
1445
- m_muscular: { bust: 0, belly: -0.02, muscle: 0.55, shoulder: 1.22, hip: 1.06, waist: 1.10, chestFlat: 0.12 },
1446
- f_muscular: { bust: 0.90, belly: -0.02, muscle: 0.38, shoulder: 1.06, hip: 1.10, waist: 0.92, chestFlat: 0 },
1447
- m_plump: { bust: 0, belly: 0.30, muscle: 0.06, shoulder: 1.12, hip: 1.20, waist: 1.16, chestFlat: 0.02 },
1448
- f_plump: { bust: 1.15, belly: 0.32, muscle: 0.02, shoulder: 1.02, hip: 1.24, waist: 1.00, chestFlat: 0 },
1626
+ m_standard: { bust: 0, belly: 0.00, muscle: 0.50, shoulder: 1.14, hip: 0.96, waist: 1.24, chestFlat: 0.10, head: 1.00 },
1627
+ f_standard: { bust: 1.00, belly: 0.04, muscle: 0.00, shoulder: 0.94, hip: 1.10, waist: 0.88, chestFlat: 0, head: 1.00 },
1628
+ m_teen: { bust: 0, belly: 0.02, muscle: 0.30, shoulder: 1.00, hip: 0.98, waist: 1.12, chestFlat: 0.08, head: 1.08 },
1629
+ f_teen: { bust: 0.62, belly: 0.03, muscle: 0.00, shoulder: 0.90, hip: 1.02, waist: 0.90, chestFlat: 0, head: 1.00 },
1630
+ m_child: { bust: 0, belly: 0.06, muscle: 0.10, shoulder: 0.86, hip: 0.88, waist: 1.06, chestFlat: 0.04, head: 1.18 },
1631
+ f_child: { bust: 0.18, belly: 0.06, muscle: 0.00, shoulder: 0.82, hip: 0.90, waist: 0.94, chestFlat: 0, head: 1.00 },
1632
+ m_slim: { bust: 0, belly: -0.02, muscle: 0.25, shoulder: 1.04, hip: 0.96, waist: 1.10, chestFlat: 0.10, head: 1.00 },
1633
+ f_slim: { bust: 0.78, belly: -0.03, muscle: 0.00, shoulder: 0.88, hip: 0.98, waist: 0.84, chestFlat: 0, head: 1.00 },
1634
+ m_muscular: { bust: 0, belly: -0.02, muscle: 0.85, shoulder: 1.30, hip: 1.08, waist: 1.20, chestFlat: 0.12, head: 0.98 },
1635
+ f_muscular: { bust: 0.90, belly: -0.02, muscle: 0.38, shoulder: 1.06, hip: 1.10, waist: 0.92, chestFlat: 0, head: 1.00 },
1636
+ m_plump: { bust: 0, belly: 0.55, muscle: 0.18, shoulder: 1.16, hip: 1.24, waist: 1.30, chestFlat: 0.02, head: 1.00 },
1637
+ f_plump: { bust: 1.15, belly: 0.32, muscle: 0.02, shoulder: 1.02, hip: 1.24, waist: 1.00, chestFlat: 0, head: 1.00 },
1449
1638
  };
1450
1639
  export function bodyShapeOf(typeId) { return BODY_SHAPE_OF[typeId] || BODY_SHAPE_OF.m_standard; }
1451
1640
 
@@ -1455,7 +1644,18 @@ function applyBodyShape(asset, pos, shape) {
1455
1644
  const bones = asset.bones;
1456
1645
  const J = asset.joints, W = asset.weights;
1457
1646
  const nv = pos.length / 3;
1458
- const { bust = 0, belly = 0, muscle = 0, shoulder = 1, hip = 1, waist = 1, chestFlat = 0 } = shape;
1647
+ const { bust = 0, belly = 0, muscle = 0, shoulder = 1, hip = 1, waist = 1, chestFlat = 0, head = 1 } = shape;
1648
+ // 形变带按「相对身高」计算,不再用绝对米:xbot 是米制(rawHeight≈1.8),
1649
+ // soldier≈44、explorer≈194 都是厘米制——绝对带在它们身上会错位到脚踝(形变静默失效)。
1650
+ // k = 该资产身高 / 基准身高 1.809(这些带最初按 xbot 调的)。
1651
+ let Hm = asset._shapeH;
1652
+ if (!Hm) {
1653
+ let mn = Infinity, mx = -Infinity;
1654
+ for (let i = 1; i < pos.length; i += 3) { const y = pos[i]; if (y < mn) mn = y; if (y > mx) mx = y; }
1655
+ Hm = (Number.isFinite(mn) && mx > mn) ? (mx - mn) : 1.8;
1656
+ asset._shapeH = Hm;
1657
+ }
1658
+ const k = Hm / 1.809; // 长度缩放(y 带中心/宽度、横向高斯宽度都用它)
1459
1659
  for (let v = 0; v < nv; v++) {
1460
1660
  // 主导骨骼
1461
1661
  let bi = -1, bw = 0;
@@ -1468,35 +1668,37 @@ function applyBodyShape(asset, pos, shape) {
1468
1668
  const isArm = /arm|hand|forearm|shoulder|clav/.test(nm);
1469
1669
  const isLeg = /thigh|leg|shin|foot/.test(nm);
1470
1670
  // 腰部横向:男性直腰(>1),女性收腰(<1)——这是男女轮廓最关键的区别
1471
- if (isTorso && /spine1|spine|hips/.test(nm) && y > 0.85 && y < 1.22) {
1472
- const gw = Math.exp(-Math.pow((y - 1.03) / 0.14, 2));
1671
+ if (isTorso && /spine1|spine|hips/.test(nm) && y > 0.47 * Hm && y < 0.675 * Hm) {
1672
+ const gw = Math.exp(-Math.pow((y - 0.5694 * Hm) / (0.0774 * Hm), 2));
1473
1673
  nx *= 1 + (waist - 1) * gw;
1474
1674
  }
1475
1675
  // 男性胸部正面压扁(女性不压)
1476
- if (chestFlat > 0 && isTorso && /chest|spine2/.test(nm) && y > 1.20 && z > 0) {
1676
+ if (chestFlat > 0 && isTorso && /chest|spine2/.test(nm) && y > 0.663 * Hm && z > 0) {
1477
1677
  nz *= 1 - chestFlat;
1478
1678
  }
1479
1679
  // 肩胯宽度
1480
1680
  if (isTorso && /chest|spine2|spine1|spine|clav/.test(nm)) nx *= shoulder;
1481
1681
  if (isTorso && /hips/.test(nm)) { nx *= hip; nz *= hip * 0.98; }
1482
1682
  // 腹部体积:腹部(y 在中段)向前鼓
1483
- if (belly > 0 && isTorso && y > 0.75 && y < 1.45) {
1484
- const g = Math.exp(-Math.pow((y - 1.08) / 0.22, 2));
1485
- nz += belly * 0.085 * g * Math.exp(-Math.pow(x / 0.19, 2));
1683
+ if (belly > 0 && isTorso && y > 0.4146 * Hm && y < 0.8016 * Hm) {
1684
+ const g = Math.exp(-Math.pow((y - 0.597 * Hm) / (0.1216 * Hm), 2));
1685
+ nz += belly * 0.085 * k * g * Math.exp(-Math.pow(x / (0.19 * k), 2));
1486
1686
  nx *= 1 + belly * 0.10 * g;
1487
1687
  }
1488
1688
  if (belly < 0 && isTorso) { nx *= 1 + belly * 0.25; nz *= 1 + belly * 0.25; }
1489
1689
  // 胸部体积(女性):胸前两侧隆起
1490
- if (bust > 0 && isTorso && y > 1.14 && y < 1.52 && z > 0) {
1491
- const gg = Math.exp(-Math.pow((y - 1.33) / 0.10, 2));
1492
- const lobe = Math.exp(-Math.pow((Math.abs(x) - 0.075) / 0.055, 2));
1493
- nz += bust * 0.075 * gg * lobe;
1690
+ if (bust > 0 && isTorso && y > 0.630 * Hm && y < 0.840 * Hm && z > 0) {
1691
+ const gg = Math.exp(-Math.pow((y - 0.735 * Hm) / (0.0553 * Hm), 2));
1692
+ const lobe = Math.exp(-Math.pow((Math.abs(x) - 0.075 * k) / (0.055 * k), 2));
1693
+ nz += bust * 0.075 * k * gg * lobe;
1494
1694
  }
1495
- // 肌肉围度:四肢加粗
1695
+ // 肌肉围度:四肢加粗(乘数 0.55 对齐 storyai 的 15-27% 单部位幅度)
1496
1696
  if (muscle > 0 && (isArm || isLeg)) {
1497
- const k = 1 + muscle * (/forearm|hand|shin|foot/.test(nm) ? 0.10 : 0.22);
1697
+ const k = 1 + muscle * (/forearm|hand|shin|foot/.test(nm) ? 0.16 : 0.55);
1498
1698
  nx *= k; nz *= k;
1499
1699
  }
1700
+ // 头部缩放不在这里做:顶点级缩放会把头拉长(骨骼没跟上去 → 视觉「飞头」)。
1701
+ // 改由 skinModel 在蒙皮前对 Head/Neck 骨骼矩阵缩放(applyHeadBoneScale)。
1500
1702
  if (nx !== x || ny !== y || nz !== z) { pos[v * 3] = nx; pos[v * 3 + 1] = ny; pos[v * 3 + 2] = nz; }
1501
1703
  }
1502
1704
  }
@@ -2364,13 +2566,35 @@ export function skinModel(asset, clipName, time, outPos, outNrm, shape) {
2364
2566
  nrm[v * 3] = nx / l; nrm[v * 3 + 1] = ny / l; nrm[v * 3 + 2] = nz / l;
2365
2567
  }
2366
2568
  if (shape) applyBodyShape(asset, pos, shape);
2569
+ // 头部放大:顶点级按高度 smoothstep 渐变(颈根 1× → 头顶 k×)。
2570
+ // 骨骼级缩放会把颈部顶点推离、在 0.84–0.86H 留空档 → 头身分离(用户实测)。
2571
+ if (shape && Number(shape.head) && Number(shape.head) !== 1) headVertexScale(pos, Number(shape.head));
2367
2572
  return { pos, nrm };
2368
2573
  }
2369
2574
 
2575
+ /** 头部顶点级缩放:以身高 78%(颈根)为枢轴,向头顶 smoothstep 渐变放大。
2576
+ * 保证颈→头网格连续:颈根不动、越靠头顶放大越多,不产生顶点空档(骨骼级缩放会断开)。 */
2577
+ function headVertexScale(pos, k) {
2578
+ if (!Number.isFinite(k) || k === 1 || !pos || !pos.length) return;
2579
+ const nv = pos.length / 3;
2580
+ let maxY = -Infinity;
2581
+ for (let v = 0; v < nv; v++) { const y = pos[v * 3 + 1]; if (y > maxY) maxY = y; }
2582
+ if (!Number.isFinite(maxY) || maxY <= 0) return;
2583
+ const pivot = maxY * 0.78;
2584
+ const span = Math.max(1e-6, maxY - pivot);
2585
+ for (let v = 0; v < nv; v++) {
2586
+ const y = pos[v * 3 + 1];
2587
+ if (y <= pivot) continue;
2588
+ const t2 = Math.min(1, (y - pivot) / span);
2589
+ const w = t2 * t2 * (3 - 2 * t2);
2590
+ const kk = 1 + (k - 1) * w;
2591
+ pos[v * 3] *= kk;
2592
+ pos[v * 3 + 1] = pivot + (y - pivot) * kk;
2593
+ pos[v * 3 + 2] *= kk;
2594
+ }
2595
+ }
2596
+
2370
2597
  /**
2371
- * 播放头 → 场景副本:模型对象的动作时间跟随播放头,带关键帧的相机做位置/注视点插值。
2372
- * 预演与导出视频都走这一个函数,保证"看到的"和"导出的"一致。
2373
- */
2374
2598
  /* ============================================================
2375
2599
  * 运动路径(对照 Blender BKE_where_on_path + Follow Path 约束)
2376
2600
  * 关键帧串成 Catmull-Rom 样条 → 按时间参数在曲线上取「位置 + 切线」
@@ -3226,15 +3450,17 @@ export function applyPlayhead(scene, t) {
3226
3450
  const bl = poseBlendAt(BONE_POSES, o.poseAnim, poseSampleTime);
3227
3451
  if (bl) { next.poseBlend = bl; next.poseId = null; }
3228
3452
  }
3229
- // 姿态根位移(下蹲/轻功/倒地需要整体升降):直接作用在位置上,最可靠
3453
+ // 姿态根位移(下蹲/轻功/倒地需要整体升降):**只在骨骼层生效**(见 applyBonePoseToLocal
3454
+ // 的 ①b2 段:把 Hips 整体上下平移)。这里绝不能再往 pos.y 里加一次——
3455
+ // 那是重复应用,而且会把物体的逻辑原点(人物脚底)污染成"髋部高度",
3456
+ // 导致 gizmo 轴心悬空、路径/关键帧的 y 基准也跟着漂。
3457
+ // (历史 bug:这一行曾把 rootY 加到 next.pos.y 上,站立时 pos.y≈1.04,轴心飘到腰腹。)
3458
+ // 姿态中心偏移(**仅供需要「跟随身体」的场合**,例如要让 gizmo 贴住倒地的人):
3459
+ // 单独存成 poseCenterY,绝不并进 pos.y —— pos.y 永远是物体原点。
3230
3460
  {
3231
- let ry = 0;
3232
- if (o.poseId) { const sp = BONE_POSES.find((p) => p.id === o.poseId); if (sp && sp.rootY) ry = Number(sp.rootY) || 0; }
3233
- if (Array.isArray(o.poseAnim) && o.poseAnim.length) {
3234
- const bl = poseBlendAt(BONE_POSES, o.poseAnim, poseSampleTime);
3235
- if (bl) { let acc = 0, w = 0; for (const e of bl) { acc += (Number(e.rootY) || 0) * e.w; w += e.w; } ry = w > 0 ? acc / w : 0; }
3236
- }
3237
- if (ry) next.pos = v3(next.pos.x, next.pos.y + ry, next.pos.z);
3461
+ // 姿态根位移只用于**渲染骨骼**(applyBonePoseToLocal 负责),此处不再写 pos.y。
3462
+ // 保留一个显式的空块形式以免误读:确需场景级整体升降时,请改 sh 的 ground 或加关键帧,
3463
+ // 不要把姿态位移混进物体原点。
3238
3464
  }
3239
3465
  // 姿态中心偏移(供 gizmo / 拾取跟随身体,而非留在地面原点)
3240
3466
  if (next.kind === 'model' && gltfAssetOf(next.assetId)) {
@@ -3368,19 +3594,26 @@ export function resetView(scene) {
3368
3594
  * 不必真的重建 12 套网格(那样包体积会爆炸)。
3369
3595
  */
3370
3596
  export const MANNEQUIN_TYPES = [
3371
- { id: 'm_standard', zh: '男性 · 标准', en: 'Male standard', h: 1.78, w: 1.00, d: 1.00 , color: '#7ba7d7' },
3372
- { id: 'f_standard', zh: '女性 · 标准', en: 'Female standard', h: 1.66, w: 0.92, d: 0.94 , color: '#d78fb0' },
3373
- { id: 'm_teen', zh: '男性 · 少年', en: 'Male teen', h: 1.60, w: 0.90, d: 0.92 , color: '#6fb3a0' },
3374
- { id: 'f_teen', zh: '女性 · 少年', en: 'Female teen', h: 1.54, w: 0.86, d: 0.90 , color: '#c99bd6' },
3375
- { id: 'm_child', zh: '男性 · 儿童', en: 'Male child', h: 1.18, w: 0.80, d: 0.86 , color: '#8fc46a' },
3376
- { id: 'f_child', zh: '女性 · 儿童', en: 'Female child', h: 1.15, w: 0.78, d: 0.85 , color: '#e0b062' },
3377
- { id: 'm_slim', zh: '男性 · 纤细', en: 'Male slim', h: 1.80, w: 0.88, d: 0.90 , color: '#7f9fd0' },
3378
- { id: 'f_slim', zh: '女性 · 纤细', en: 'Female slim', h: 1.68, w: 0.84, d: 0.88 , color: '#cf9fc0' },
3379
- { id: 'm_muscular', zh: '男性 · 健壮', en: 'Male muscular', h: 1.82, w: 1.12, d: 1.08 , color: '#d98a6a' },
3380
- { id: 'f_muscular', zh: '女性 · 健壮', en: 'Female muscular', h: 1.70, w: 1.02, d: 1.02 , color: '#c07f9c' },
3381
- { id: 'm_plump', zh: '男性 · 丰腴', en: 'Male plump', h: 1.74, w: 1.18, d: 1.14 , color: '#9aa6c4' },
3382
- { id: 'f_plump', zh: '女性 · 丰腴', en: 'Female plump', h: 1.62, w: 1.14, d: 1.12 , color: '#c49ab0' },
3597
+ { id: 'm_standard', zh: '男性 · 标准', en: 'Male standard', h: 1.78, w: 1.00, d: 1.00 , color: '#2f7fe8' },
3598
+ { id: 'f_standard', zh: '女性 · 标准', en: 'Female standard', h: 1.66, w: 0.92, d: 0.94 , color: '#e857a8' },
3599
+ { id: 'm_teen', zh: '男性 · 少年', en: 'Male teen', h: 1.15, w: 0.90, d: 0.92 , color: '#17b877' },
3600
+ { id: 'f_teen', zh: '女性 · 少年', en: 'Female teen', h: 1.12, w: 0.86, d: 0.90 , color: '#8b5cf6' },
3601
+ { id: 'm_child', zh: '男性 · 儿童', en: 'Male child', h: 0.80, w: 0.90, d: 0.96 , color: '#7cc93f' },
3602
+ { id: 'f_child', zh: '女性 · 儿童', en: 'Female child', h: 0.80, w: 0.88, d: 0.95 , color: '#f0a832' },
3603
+ { id: 'm_slim', zh: '男性 · 纤细', en: 'Male slim', h: 1.80, w: 0.88, d: 0.90 , color: '#38a8e8' },
3604
+ { id: 'f_slim', zh: '女性 · 纤细', en: 'Female slim', h: 1.68, w: 0.84, d: 0.88 , color: '#e85d6f' },
3605
+ { id: 'm_muscular', zh: '男性 · 健壮', en: 'Male muscular', h: 1.82, w: 1.12, d: 1.08 , color: '#f07818' },
3606
+ { id: 'f_muscular', zh: '女性 · 健壮', en: 'Female muscular', h: 1.70, w: 1.02, d: 1.02 , color: '#d83050' },
3607
+ { id: 'm_plump', zh: '男性 · 丰腴', en: 'Male plump', h: 1.74, w: 1.18, d: 1.14 , color: '#5865d8' },
3608
+ { id: 'f_plump', zh: '女性 · 丰腴', en: 'Female plump', h: 1.62, w: 1.14, d: 1.12 , color: '#a05fd8' },
3383
3609
  ];
3610
+
3611
+ /** 体型的标志色(MANNEQUIN_TYPES.color):ob.color 为空时的默认着色,
3612
+ * 让 12 体型在视口/缩略图里像 storyai 资产页那样一眼可辨。 */
3613
+ export function mannequinColorOf(typeId) {
3614
+ const t2 = (MANNEQUIN_TYPES || []).find((x) => x.id === typeId);
3615
+ return (t2 && t2.color) || '';
3616
+ }
3384
3617
  export function mannequinTypeOf(id) { return MANNEQUIN_TYPES.find((t) => t.id === id) || MANNEQUIN_TYPES[0]; }
3385
3618
 
3386
3619
  /** 资产包围盒(用于自动归一化到人高 / 落地)。 */
@@ -5403,7 +5636,7 @@ export function createDirectorGlRenderer(canvas) {
5403
5636
  if (locSkin && locSkin.uRoughness) gl.uniform1f(locSkin.uRoughness, mat && mat.roughness !== undefined ? Number(mat.roughness) : 1);
5404
5637
  if (locSkin && locSkin.uMetallic) gl.uniform1f(locSkin.uMetallic, mat && mat.metallic !== undefined ? Number(mat.metallic) : 0);
5405
5638
  if (locSkin && locSkin.uSpecular) gl.uniform1f(locSkin.uSpecular, mat && mat.specular !== undefined ? Number(mat.specular) : 0.5);
5406
- const cc3 = directorHexToRgb(ob.color || CLAY);
5639
+ const cc3 = directorHexToRgb(ob.color || mannequinColorOf(ob.typeId) || CLAY);
5407
5640
  if (drawDynamic(sk.pos, sk.nrm, asset.indices, model, cc3, !!(o.selectedId && ob.id === o.selectedId), viewProj, lightPack, Number(env.exposure) || 1)) {
5408
5641
  stats.faces += asset.tris;
5409
5642
  stats.tris += asset.tris;
@@ -5419,7 +5652,7 @@ export function createDirectorGlRenderer(canvas) {
5419
5652
  const rec = uploadSkin(ob.assetId, asset);
5420
5653
  const t = Number(ob.animTime) || 0;
5421
5654
  const bones = asset.bones ? boneMatrices(asset, t) : null;
5422
- const cc2 = directorHexToRgb(ob.color || CLAY);
5655
+ const cc2 = directorHexToRgb(ob.color || mannequinColorOf(ob.typeId) || CLAY);
5423
5656
  const model = directorMat4Model(ob.pos, Number(ob.rotY) || 0, v3(Number(ob.s) || 1, Number(ob.s) || 1, Number(ob.s) || 1));
5424
5657
  if (drawSkin(asset, rec, model, cc2, viewProj, lightPack, Number(env.exposure) || 1, !!(o.selectedId && ob.id === o.selectedId), bones)) {
5425
5658
  stats.faces += asset.tris;
@@ -5442,7 +5675,7 @@ export function createDirectorGlRenderer(canvas) {
5442
5675
  gl.vertexAttribPointer(loc.aNormal, 3, gl.FLOAT, false, 0, 0);
5443
5676
  gl.uniformMatrix4fv(loc.uModel, false, directorMat4Identity());
5444
5677
  gl.uniformMatrix3fv(loc.uNormalMat, false, directorMat3NormalFrom(v3(0, 0, 0), 0, v3(1, 1, 1)));
5445
- const cc = directorHexToRgb(ob.color || CLAY);
5678
+ const cc = directorHexToRgb(ob.color || mannequinColorOf(ob.typeId) || CLAY);
5446
5679
  gl.uniform3f(loc.uColor, cc[0], cc[1], cc[2]);
5447
5680
  gl.uniform1f(loc.uSelected, (o.selectedId && ob.id === o.selectedId) ? 1 : 0);
5448
5681
  gl.drawArrays(gl.TRIANGLES, 0, rec.count);
@@ -5554,13 +5787,10 @@ export function createDirectorGlRenderer(canvas) {
5554
5787
  if (o.gizmo && o.selectedId) {
5555
5788
  const sel = (scene.objects || []).find((x) => x.id === o.selectedId);
5556
5789
  if (sel) {
5557
- // 轴心默认在物体原点(人物 = 脚底中心),轴朝向由 objectAxes(sel) 给出
5790
+ // 轴心恒 = 物体自身坐标系原点(人物 = 脚底中心),轴朝向由 objectAxes(sel) 给出
5558
5791
  // → 物体旋转时轴跟着转(人物倒地后 z 轴仍指向头),不是固定世界坐标。
5559
- // 「跟随身体」是显式开关:仅当选中项或场景打开 gizmoFollowBody 时才抬到髋高。
5560
- const followBody = !!(sel.gizmoFollowBody || o.gizmoFollowBody);
5561
- const origin = followBody
5562
- ? v3(sel.pos.x, sel.pos.y + (Number(sel.poseCenterY) || 0), sel.pos.z)
5563
- : v3(sel.pos.x, sel.pos.y, sel.pos.z);
5792
+ // 绝不随姿态/髋高上浮:任何"智能抬升"都会让轴悬空(0.19.3/0.19.4 的教训)。
5793
+ const origin = v3(sel.pos.x, sel.pos.y, sel.pos.z);
5564
5794
  // 屏幕恒定大小(Blender: wm_gizmo_calculate_scale):轴长 105 CSS px,环 0.62×轴长。
5565
5795
  // 尺寸唯一来源 gizmoMetrics,拾取用同一个函数 → 不可能再出现「画的位置和点不中」。
5566
5796
  const gm = gizmoMetrics(cam, sel, hCss, origin);
@@ -5703,7 +5933,7 @@ export function directorRenderScene(ctx, w, h, scene, opts) {
5703
5933
  (scene.objects || []).forEach((ob) => {
5704
5934
  const fs = directorObjectFaces(ob);
5705
5935
  stats.faces += fs.length;
5706
- fs.forEach((f) => items.push({ f, color: ob.color || CLAY, base: ob }));
5936
+ fs.forEach((f) => items.push({ f, color: ob.color || mannequinColorOf(ob.typeId) || CLAY, base: ob }));
5707
5937
  });
5708
5938
  if (false && env.groundVisible !== false) {
5709
5939
  const s = 400, op = 0; // 不画地面填充,只画网格
@@ -5947,12 +6177,8 @@ export function directorPickGizmo(scene, selectedId, w, h, sx, sy) {
5947
6177
  if (!cam) return null;
5948
6178
  // 与绘制完全同源:精确屏幕投影 + 同一个 gizmoMetrics(w/h 都是 CSS 尺寸)
5949
6179
  const project = (p) => directorScreenOf(cam, w, h, p);
5950
- // 轴心规则必须与绘制一致:默认物体原点,只有显式打开「跟随身体」才抬到髋高
5951
- const pickFollow = !!(sel.gizmoFollowBody || scene.gizmoFollowBody);
5952
- const pickOrigin = pickFollow
5953
- ? v3(sel.pos.x, sel.pos.y + (Number(sel.poseCenterY) || 0), sel.pos.z)
5954
- : v3(sel.pos.x, sel.pos.y, sel.pos.z);
5955
- const gm = gizmoMetrics(cam, sel, h, pickOrigin);
6180
+ // 与绘制完全同源:轴心恒 = 物体原点(规则必须与绘制一字不差,否则点不中画出来的轴)
6181
+ const gm = gizmoMetrics(cam, sel, h, v3(sel.pos.x, sel.pos.y, sel.pos.z));
5956
6182
  const origin = gm.origin, L = gm.L, R = gm.R;
5957
6183
  const ax = objectAxes(sel);
5958
6184
  const AXES = [['x', ax.x], ['y', ax.y], ['z', ax.z]];