@developerz.ai/ts23d 0.1.0 → 0.2.0

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 (162) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/CLAUDE.md +1 -1
  3. package/README.md +10 -5
  4. package/dist/{chunk-a8fxakcj.js → chunk-6b05mnmx.js} +2 -2
  5. package/dist/{chunk-kgvbpqqw.js → chunk-7rtfjh9f.js} +6 -5
  6. package/dist/chunk-7rtfjh9f.js.map +10 -0
  7. package/dist/{chunk-1ypfs7ga.js → chunk-8j4c28jt.js} +790 -276
  8. package/dist/chunk-8j4c28jt.js.map +46 -0
  9. package/dist/{chunk-wtqhq529.js → chunk-d1fv6hrm.js} +6 -6
  10. package/dist/{chunk-wtqhq529.js.map → chunk-d1fv6hrm.js.map} +1 -1
  11. package/dist/{chunk-gcyp9b4d.js → chunk-dbq4qf52.js} +362 -215
  12. package/dist/chunk-dbq4qf52.js.map +25 -0
  13. package/dist/{chunk-xnt2sdhf.js → chunk-drna2rny.js} +2 -2
  14. package/dist/{chunk-ravbb06q.js → chunk-dxvypr7y.js} +2 -2
  15. package/dist/{chunk-11v28977.js → chunk-f6yme003.js} +85 -71
  16. package/dist/{chunk-11v28977.js.map → chunk-f6yme003.js.map} +6 -6
  17. package/dist/{chunk-4ddzsqtc.js → chunk-hg7pf34j.js} +4 -3
  18. package/dist/{chunk-4ddzsqtc.js.map → chunk-hg7pf34j.js.map} +5 -5
  19. package/dist/{chunk-w1zyhj40.js → chunk-jq4e17tj.js} +2 -2
  20. package/dist/{chunk-kvcep7ft.js → chunk-y63c6zcv.js} +967 -218
  21. package/dist/chunk-y63c6zcv.js.map +53 -0
  22. package/dist/cli/args.d.ts +7 -1
  23. package/dist/cli/bin.js +66 -25
  24. package/dist/cli/bin.js.map +6 -5
  25. package/dist/cli/manifest.d.ts +5 -1
  26. package/dist/cli/texture-cache.d.ts +4 -0
  27. package/dist/core/definition.d.ts +7 -0
  28. package/dist/core/errors.d.ts +1 -1
  29. package/dist/core/index.js +1 -1
  30. package/dist/export/credits.d.ts +4 -0
  31. package/dist/export/glb-options.d.ts +48 -0
  32. package/dist/export/glb.d.ts +5 -35
  33. package/dist/export/hierarchy.d.ts +23 -0
  34. package/dist/export/index.d.ts +2 -1
  35. package/dist/export/index.js +21 -7
  36. package/dist/export/index.js.map +1 -1
  37. package/dist/export/lod-coverage.d.ts +12 -0
  38. package/dist/export/metadata.d.ts +2 -0
  39. package/dist/export/version.d.ts +2 -2
  40. package/dist/grammar/index.js +2 -2
  41. package/dist/index.js +63 -17
  42. package/dist/index.js.map +1 -1
  43. package/dist/kinds/head-armour.d.ts +3 -7
  44. package/dist/kinds/head-bust.d.ts +3 -2
  45. package/dist/kinds/head-face.d.ts +3 -1
  46. package/dist/kinds/head-garb.d.ts +4 -0
  47. package/dist/kinds/head-mesh.d.ts +4 -0
  48. package/dist/kinds/head-mouth.d.ts +4 -0
  49. package/dist/kinds/head-params.d.ts +9 -1
  50. package/dist/kinds/head-tone.d.ts +10 -0
  51. package/dist/kinds/head-torso.d.ts +35 -0
  52. package/dist/kinds/index.js +6 -6
  53. package/dist/material/cc0.d.ts +4 -0
  54. package/dist/material/define.d.ts +34 -0
  55. package/dist/material/generator.d.ts +1 -1
  56. package/dist/material/index.d.ts +8 -3
  57. package/dist/material/index.js +39 -7
  58. package/dist/material/index.js.map +1 -1
  59. package/dist/material/library.d.ts +7 -0
  60. package/dist/material/material-def.d.ts +30 -0
  61. package/dist/material/paint/canvas.d.ts +7 -0
  62. package/dist/material/paint/default-sheet.d.ts +3 -1
  63. package/dist/material/paint/fabric.d.ts +4 -0
  64. package/dist/material/paint/image-strip.d.ts +4 -0
  65. package/dist/material/paint/kit.d.ts +5 -0
  66. package/dist/material/paint/metal.d.ts +3 -0
  67. package/dist/material/paint/orm.d.ts +1 -1
  68. package/dist/material/paint/strip.d.ts +1 -1
  69. package/dist/material/paint/types.d.ts +32 -2
  70. package/dist/material/palette.d.ts +2 -0
  71. package/dist/material/slots.d.ts +5 -0
  72. package/dist/material/texture-source.d.ts +34 -0
  73. package/dist/material/trim-sheet.d.ts +20 -4
  74. package/dist/neural/index.js +2 -2
  75. package/dist/passes/index.js +2 -2
  76. package/dist/primitives/index.js +3 -3
  77. package/dist/qa/index.js +2 -2
  78. package/dist/render/beauty.d.ts +1 -1
  79. package/dist/render/index.js +3 -3
  80. package/dist/render/lighting.d.ts +7 -0
  81. package/dist/render/shade.d.ts +1 -1
  82. package/dist/testing/index.js +5 -5
  83. package/docs/README.md +4 -0
  84. package/docs/architecture/gltf-capabilities.md +38 -1
  85. package/docs/architecture/materials.md +151 -0
  86. package/docs/idea/10-sculpting.md +166 -0
  87. package/docs/idea/11-materials-authoring.md +132 -0
  88. package/docs/idea/12-blender-bridge.md +136 -0
  89. package/docs/idea/13-asset-pipeline-research.md +89 -0
  90. package/docs/plans/roadmap.md +12 -0
  91. package/package.json +2 -1
  92. package/recipes/boxcar/README.md +7 -0
  93. package/recipes/boxcar/boxcar.ts +118 -34
  94. package/recipes/goblin_head/README.md +27 -14
  95. package/recipes/goblin_head/goblin_head.ts +11 -9
  96. package/recipes/human_head/README.md +8 -4
  97. package/recipes/human_head/human_head.ts +4 -3
  98. package/recipes/material_swatches/material_swatches.ts +11 -5
  99. package/recipes/orc_head/README.md +32 -20
  100. package/recipes/orc_head/orc_head.ts +9 -7
  101. package/src/cli/args.ts +16 -3
  102. package/src/cli/manifest.ts +6 -0
  103. package/src/cli/run.ts +33 -9
  104. package/src/cli/texture-cache.ts +24 -0
  105. package/src/core/asset.ts +1 -0
  106. package/src/core/definition.ts +7 -0
  107. package/src/core/errors.ts +6 -1
  108. package/src/export/credits.ts +25 -0
  109. package/src/export/glb-options.ts +53 -0
  110. package/src/export/glb.ts +100 -113
  111. package/src/export/hierarchy.ts +122 -0
  112. package/src/export/index.ts +17 -1
  113. package/src/export/lod-coverage.ts +71 -0
  114. package/src/export/metadata.ts +3 -0
  115. package/src/export/version.ts +1 -1
  116. package/src/kinds/head-armour.ts +103 -73
  117. package/src/kinds/head-bust.ts +49 -86
  118. package/src/kinds/head-face.ts +94 -74
  119. package/src/kinds/head-garb.ts +181 -0
  120. package/src/kinds/head-hair.ts +74 -33
  121. package/src/kinds/head-mesh.ts +43 -5
  122. package/src/kinds/head-mouth.ts +111 -0
  123. package/src/kinds/head-params.ts +11 -2
  124. package/src/kinds/head-presets.ts +30 -27
  125. package/src/kinds/head-tone.ts +52 -0
  126. package/src/kinds/head-torso.ts +230 -0
  127. package/src/kinds/head.ts +8 -3
  128. package/src/material/cc0.ts +41 -0
  129. package/src/material/define.ts +130 -0
  130. package/src/material/generator.ts +15 -8
  131. package/src/material/index.ts +34 -3
  132. package/src/material/library.ts +196 -0
  133. package/src/material/material-def.ts +84 -0
  134. package/src/material/paint/canvas.ts +11 -1
  135. package/src/material/paint/compose.ts +1 -1
  136. package/src/material/paint/decode-png.ts +19 -13
  137. package/src/material/paint/default-sheet.ts +40 -28
  138. package/src/material/paint/fabric.ts +63 -0
  139. package/src/material/paint/image-strip.ts +88 -0
  140. package/src/material/paint/kit.ts +37 -0
  141. package/src/material/paint/masks.ts +3 -1
  142. package/src/material/paint/metal.ts +109 -0
  143. package/src/material/paint/orm.ts +14 -1
  144. package/src/material/paint/planks.ts +49 -4
  145. package/src/material/paint/strip.ts +35 -29
  146. package/src/material/paint/types.ts +45 -2
  147. package/src/material/paint/validate.ts +19 -3
  148. package/src/material/palette.ts +3 -0
  149. package/src/material/slots.ts +18 -0
  150. package/src/material/texture-source.ts +86 -0
  151. package/src/material/trim-sheet.ts +79 -13
  152. package/src/render/beauty.ts +2 -2
  153. package/src/render/lighting.ts +26 -0
  154. package/src/render/shade.ts +10 -5
  155. package/dist/chunk-1ypfs7ga.js.map +0 -42
  156. package/dist/chunk-gcyp9b4d.js.map +0 -22
  157. package/dist/chunk-kgvbpqqw.js.map +0 -10
  158. package/dist/chunk-kvcep7ft.js.map +0 -43
  159. /package/dist/{chunk-a8fxakcj.js.map → chunk-6b05mnmx.js.map} +0 -0
  160. /package/dist/{chunk-xnt2sdhf.js.map → chunk-drna2rny.js.map} +0 -0
  161. /package/dist/{chunk-ravbb06q.js.map → chunk-dxvypr7y.js.map} +0 -0
  162. /package/dist/{chunk-w1zyhj40.js.map → chunk-jq4e17tj.js.map} +0 -0
@@ -7,6 +7,7 @@ import type { Vec3 } from '../core/index.ts';
7
7
  import type { SculptEdit } from '../primitives/index.ts';
8
8
  import { alongX, alongZ, unit } from './head-geom.ts';
9
9
  import type { HeadLayout } from './head-layout.ts';
10
+ import { mouthEdits } from './head-mouth.ts';
10
11
  import type { HeadParams } from './head-params.ts';
11
12
 
12
13
  /** Brain case + face plate: the forms the eyes are seated against (head.ts probes them). */
@@ -24,27 +25,40 @@ export function primaryEdits(l: HeadLayout): SculptEdit[] {
24
25
  ];
25
26
  }
26
27
 
27
- export function skullEdits(p: HeadParams, l: HeadLayout): SculptEdit[] {
28
+ /** `cell`: the sculpt cell (head units). Far LODs drop the teeth and floor the nose's radii at
29
+ * about 0.6 cells so a long nose survives. */
30
+ export function skullEdits(p: HeadParams, l: HeadLayout, cell = 0): SculptEdit[] {
31
+ const fine = cell < 0.03;
32
+ const tipR = Math.max(p.nose.tip, cell * 0.6);
28
33
  const c = p.colors;
29
34
  const cx = l.cranium.at[0];
30
35
  const [ex, ey, ez] = l.eye;
31
36
  const r = l.eyeR;
32
- const lipT = p.mouth.lips;
33
37
  const mw = p.mouth.width;
34
38
  const [ax, ay, az] = l.angle;
35
39
  const [hx, hz] = l.hinge;
36
40
  const chin = l.chin;
37
41
  const tipX = l.faceX + 0.06 + p.nose.length * 0.25 + p.nose.bridge + p.jaw.muzzle * 0.5;
38
42
  const tipZ = l.noseZ + 0.035 - p.nose.hook;
43
+ const bIn: Vec3 = [ex + r * 0.75, 0.03, l.browZ - p.brow.scowl];
44
+ const bOut: Vec3 = [ex + r * 0.05, ey + r * 1.3, l.browZ + p.brow.scowl * 0.5];
39
45
  const e: SculptEdit[] = [
40
46
  // Primary: brain case, face plate, cheek bones, muzzle, mandible, chin, neck.
41
47
  ...primaryEdits(l),
42
48
  {
43
49
  shape: 'ellipsoid',
44
50
  at: [ex - 0.03, (ey + 0.06) * p.cheeks.width, ez - r - 0.065 * p.cheeks.size],
45
- size: [0.09 * p.cheeks.size, 0.08 * p.cheeks.size, 0.065 * p.cheeks.size],
51
+ size: [0.075 * p.cheeks.size, 0.07 * p.cheeks.size, 0.055 * p.cheeks.size],
46
52
  rotate: [0, 0, 25],
47
- blend: 0.09,
53
+ blend: 0.08,
54
+ },
55
+ // Zygomatic arch: the cheekbone ridge running back from under the eye towards the ear.
56
+ {
57
+ shape: 'capsule',
58
+ at: [ex - r * 0.2, (ey + 0.05) * p.cheeks.width, ez - r - 0.03],
59
+ to: [cx + 0.06, l.cranium.r[1] * 0.92, ez - 0.07],
60
+ size: 0.028 * p.cheeks.size,
61
+ blend: 0.05,
48
62
  },
49
63
  // Maxilla: fills the midface between cheekbones, nose and muzzle.
50
64
  {
@@ -77,9 +91,10 @@ export function skullEdits(p: HeadParams, l: HeadLayout): SculptEdit[] {
77
91
  },
78
92
  { shape: 'sphere', at: [ax + 0.02, ay * 0.95, az], size: 0.065, blend: 0.07 },
79
93
  {
80
- shape: 'ellipsoid',
94
+ shape: 'roundBox',
81
95
  at: chin,
82
- size: [0.065 * p.jaw.chin, 0.09 * p.jaw.chin, 0.07 * p.jaw.chin],
96
+ size: [0.13 * p.jaw.chin, 0.18 * p.jaw.chin, 0.14 * p.jaw.chin],
97
+ round: 0.06 * p.jaw.chin,
83
98
  blend: 0.06,
84
99
  },
85
100
  {
@@ -89,13 +104,32 @@ export function skullEdits(p: HeadParams, l: HeadLayout): SculptEdit[] {
89
104
  size: p.neck.radius,
90
105
  blend: 0.1,
91
106
  },
92
- // Secondary: sockets, brow, lids.
107
+ // Sternocleidomastoid: the cords from behind the ear to the throat.
93
108
  {
94
109
  shape: 'capsule',
95
- at: [ex + r * 0.75, 0.025, l.browZ - p.brow.scowl],
96
- to: [ex + r * 0.1, ey + r * 1.25, l.browZ + p.brow.scowl * 0.5],
97
- size: p.brow.ridge,
98
- blend: 0.06,
110
+ at: [cx - 0.03, l.cranium.r[1] * 0.68, ez - 0.28],
111
+ to: [cx + p.neck.radius * 0.6, 0.05, l.base + 0.74],
112
+ size: p.neck.radius * 0.26,
113
+ blend: 0.05,
114
+ },
115
+ // Secondary: brow shelf (a flattened bar along the brow line, lowered inside by the scowl)
116
+ // with the glabella knot between the eyes, then the sockets and lids.
117
+ {
118
+ shape: 'ellipsoid',
119
+ at: [(bIn[0] + bOut[0]) / 2, (bIn[1] + bOut[1]) / 2, (bIn[2] + bOut[2]) / 2],
120
+ size: [
121
+ Math.hypot(bOut[0] - bIn[0], bOut[1] - bIn[1], bOut[2] - bIn[2]) / 2 + p.brow.ridge * 0.6,
122
+ p.brow.ridge * 1.25,
123
+ p.brow.ridge * 0.85,
124
+ ],
125
+ rotate: alongX([bOut[0] - bIn[0], bOut[1] - bIn[1], bOut[2] - bIn[2]]),
126
+ blend: 0.05,
127
+ },
128
+ {
129
+ shape: 'sphere',
130
+ at: [bIn[0] - 0.01, 0, bIn[2] - p.brow.ridge * 0.15],
131
+ size: p.brow.ridge * 1.05,
132
+ blend: 0.05,
99
133
  },
100
134
  { shape: 'sphere', at: l.eye, size: r * 1.12, op: 'subtract', blend: r * 0.5, color: c.shade },
101
135
  ];
@@ -121,66 +155,61 @@ export function skullEdits(p: HeadParams, l: HeadLayout): SculptEdit[] {
121
155
  };
122
156
  e.push(lid(p.eyes.lid, true), lid(p.eyes.lowerLid, false));
123
157
  // Nose: bridge (humped by `hook`), tip, wings, nostrils.
158
+ const long = p.nose.bridge > 0.06 ? 0.65 : 1;
159
+ const wing = Math.min(1.4, Math.max(0.55, p.nose.width / 0.065)) * long;
160
+ const rootR = Math.max(0.02 + p.nose.width * 0.6, cell * 0.65);
161
+ // Nostril wings sit at the nose base, however far a long nose reaches past them.
162
+ const wingX = Math.min(tipX - p.nose.tip * 1.2, l.faceX + 0.1 + p.jaw.muzzle * 0.5);
124
163
  const noseRoot: Vec3 = [ex + r * 0.4, 0, ez - r * 0.4];
125
164
  const noseDir: Vec3 = [tipX - noseRoot[0], 0, tipZ + 0.01 - noseRoot[2]];
126
165
  const midNose: Vec3 = [
127
166
  (ex + r * 0.6 + tipX) / 2,
128
167
  0,
129
- (ez - 0.02 + tipZ) / 2 + p.nose.hook * 0.6 + 0.01,
168
+ (ez - 0.02 + tipZ) / 2 + p.nose.hook * 0.3 + 0.01,
130
169
  ];
131
170
  e.push(
132
171
  {
133
172
  shape: 'cone',
134
173
  at: noseRoot,
135
- size: [0.02 + p.nose.width * 0.6, p.nose.tip * 0.8, Math.hypot(...noseDir)],
174
+ size: [rootR, tipR * 0.85, Math.hypot(...noseDir)],
136
175
  rotate: alongZ(noseDir),
137
- blend: 0.04,
176
+ blend: 0.045,
138
177
  },
139
- { shape: 'sphere', at: midNose, size: 0.02 + p.nose.width * 0.2, blend: 0.04 },
140
- { shape: 'sphere', at: [tipX, 0, tipZ], size: p.nose.tip, blend: 0.03 },
141
178
  {
142
- shape: 'ellipsoid',
143
- at: [tipX - p.nose.tip * 1.2, p.nose.width, l.noseZ + 0.02],
144
- size: [0.045, 0.035, 0.032],
145
- blend: 0.025,
179
+ shape: 'sphere',
180
+ at: midNose,
181
+ size: ((rootR + tipR) / 2) * 0.85 + Math.max(0, p.nose.hook) * 0.12,
182
+ blend: 0.05,
146
183
  },
184
+ { shape: 'sphere', at: [tipX, 0, tipZ], size: tipR, blend: 0.035 },
185
+ // A strong hook draws the tip out into a drooping point (one form, not a ball on a stick).
186
+ ...(p.nose.hook > 0.04
187
+ ? [
188
+ {
189
+ shape: 'cone',
190
+ at: [tipX, 0, tipZ],
191
+ size: [tipR * 0.9, 0.01, p.nose.tip + p.nose.hook * 1.2],
192
+ rotate: alongZ([0.6, 0, -1]),
193
+ blend: 0.03,
194
+ } satisfies SculptEdit,
195
+ ]
196
+ : []),
147
197
  {
148
198
  shape: 'ellipsoid',
149
- at: [tipX - p.nose.tip * 0.6, p.nose.width * 0.55, l.noseZ + 0.005],
150
- size: [0.022, 0.014, 0.012],
151
- op: 'subtract',
152
- blend: 0.008,
153
- color: c.mouth,
154
- },
155
- );
156
- // Lips and the mouth line (a grin opens it and lifts the corners).
157
- const g = p.mouth.grin;
158
- e.push(
159
- {
160
- shape: 'ellipsoid',
161
- at: [l.mouthX - 0.01, 0, l.mouthZ + lipT * 0.7],
162
- size: [0.032, mw, lipT],
163
- color: c.lips,
164
- blend: 0.03,
199
+ at: [wingX, p.nose.width, l.noseZ + 0.02],
200
+ size: [0.045 * wing, 0.035 * wing, 0.032 * wing],
201
+ blend: 0.025,
165
202
  },
166
203
  {
167
204
  shape: 'ellipsoid',
168
- at: [l.mouthX - 0.015 + p.jaw.underbite * 0.7, 0, l.mouthZ - lipT * 0.8],
169
- size: [0.034, mw * 0.85, lipT * 1.1],
170
- color: c.lips,
171
- blend: 0.03,
172
- },
173
- {
174
- shape: 'capsule',
175
- at: [l.mouthX + 0.05, 0, l.mouthZ],
176
- to: [l.mouthX - 0.03 - 0.02 * g, mw * 0.98, l.mouthZ + 0.012 + g * 0.05],
177
- size: 0.006 + g * 0.022,
205
+ at: [Math.min(tipX - p.nose.tip * 0.6, wingX + 0.035), p.nose.width * 0.55, l.noseZ + 0.005],
206
+ size: [0.022 * wing, 0.014 * wing, 0.012 * wing],
178
207
  op: 'subtract',
179
- blend: 0.012,
208
+ blend: 0.008,
180
209
  color: c.mouth,
181
210
  },
182
211
  );
183
- if (g > 0.15) e.push(...teethRows(p, l));
212
+ e.push(...mouthEdits(p, l, fine));
184
213
  // Nasolabial fold from the nose wing towards the mouth corner.
185
214
  const fold = unit([-0.02, mw - p.nose.width, l.mouthZ - l.noseZ]);
186
215
  e.push({
@@ -192,31 +221,22 @@ export function skullEdits(p: HeadParams, l: HeadLayout): SculptEdit[] {
192
221
  blend: 0.03,
193
222
  color: c.shade,
194
223
  });
195
- return e;
196
- }
197
-
198
- /** Upper and lower teeth, visible in a grin: rounded blocks along the dental arch. */
199
- function teethRows(p: HeadParams, l: HeadLayout): SculptEdit[] {
200
- const out: SculptEdit[] = [];
201
- const mw = p.mouth.width * 0.8;
202
- for (let i = 0; i < 4; i++) {
203
- const a = ((i + 0.5) / 4) * 0.9;
204
- const y = Math.sin(a) * mw;
205
- const x = l.mouthX - 0.025 - (1 - Math.cos(a)) * mw * 0.9;
206
- const z = l.mouthZ + 0.004 + p.mouth.grin * 0.03 * (i / 4);
207
- for (const [dz, h] of [
208
- [0.016, 0.02],
209
- [-0.016, 0.018],
210
- ] as const)
211
- out.push({
212
- shape: 'roundBox',
213
- at: [x, y, z + dz * (1 + p.mouth.grin)],
214
- size: [0.02, 0.028, h * (1 + p.mouth.grin * 0.6)],
215
- round: 0.006,
216
- rotate: [0, 0, (a * 180) / Math.PI],
217
- color: p.colors.teeth,
218
- blend: 0.003,
219
- });
224
+ // Forehead creases, painted (a groove that thin is below the sculpt cell): thin discs through
225
+ // the brow, fading where the forehead turns away.
226
+ const [, , cz] = l.cranium.at;
227
+ const [crx, , crz] = l.cranium.r;
228
+ for (let i = 0; i < (p.wrinkles ?? 0); i++) {
229
+ const z = l.browZ + 0.07 + i * 0.05;
230
+ const x = cx + crx * Math.sqrt(Math.max(0, 1 - ((z - cz) / crz) ** 2)) + 0.005;
231
+ e.push({
232
+ shape: 'ellipsoid',
233
+ at: [x, 0.06, z - 0.008],
234
+ size: [0.05, 0.08 - i * 0.012, 0.005],
235
+ rotate: [-14, -12, 0],
236
+ op: 'paint',
237
+ blend: 0.01,
238
+ color: c.shade,
239
+ });
220
240
  }
221
- return out;
241
+ return e;
222
242
  }
@@ -0,0 +1,181 @@
1
+ // The scrawny tinker's garb (bust style `vest`), layered like real clothing over the bony torso:
2
+ // a shirt shell with a V-neck (clavicles and sternal notch show through), a leather vest shell open
3
+ // down the front, a strap across the chest with a buckle, a scarf wound round the neck and knotted
4
+ // off-centre with hanging tails, and a cowl (a hood worn down) bunched on the back of the neck.
5
+ // Each layer is the torso grown a little (torsoForms) and trimmed by the same bust cut.
6
+
7
+ import type { Vec3 } from '../core/index.ts';
8
+ import type { SculptEdit } from '../primitives/index.ts';
9
+ import { alongZ, unit } from './head-geom.ts';
10
+ import type { HeadParams } from './head-params.ts';
11
+ import { type BustFrame, bustCut, torsoForms } from './head-torso.ts';
12
+
13
+ const shade = (c: Vec3, k: number): Vec3 => [c[0] * k, c[1] * k, c[2] * k];
14
+
15
+ export function vest(p: HeadParams, f: BustFrame): SculptEdit[] {
16
+ const c = p.colors;
17
+ const { cx, s, b, nz, n } = f;
18
+ const scarf = c.scarf ?? c.cloth;
19
+ const hood = c.cape ?? shade(c.cloth, 0.8);
20
+ const out: SculptEdit[] = [
21
+ // Shirt: V-neck down to the sternum.
22
+ {
23
+ group: [
24
+ { group: torsoForms(f, 0.032) },
25
+ {
26
+ shape: 'ellipsoid',
27
+ at: [cx + 0.32, 0, nz + 0.01],
28
+ size: [0.2, 0.07, 0.11],
29
+ op: 'subtract',
30
+ blend: 0.01,
31
+ },
32
+ bustCut(f),
33
+ ],
34
+ color: c.cloth,
35
+ blend: 0.006,
36
+ },
37
+ // Vest: open down the front, the opening widening to the hem.
38
+ {
39
+ group: [
40
+ { group: torsoForms(f, 0.065) },
41
+ {
42
+ shape: 'roundBox',
43
+ at: [cx + 0.4, 0, b + 0.3],
44
+ size: [0.5, 0.3, 1],
45
+ rotate: [0, -8, 0],
46
+ round: 0.08,
47
+ op: 'subtract',
48
+ blend: 0.02,
49
+ },
50
+ // Arm holes: the vest stops short of the shoulder tip, so the shirt shows there.
51
+ {
52
+ shape: 'sphere',
53
+ at: [cx - 0.03, s * 0.98, b + 0.42],
54
+ size: 0.17,
55
+ op: 'subtract',
56
+ blend: 0.02,
57
+ },
58
+ bustCut(f),
59
+ ],
60
+ color: c.leather,
61
+ blend: 0.006,
62
+ },
63
+ // Strap from the right shoulder across to the left hip, with a buckle.
64
+ {
65
+ group: [
66
+ { group: torsoForms(f, 0.088) },
67
+ {
68
+ shape: 'roundBox',
69
+ at: [cx, 0, b + 0.38],
70
+ size: [4, 4, 0.08],
71
+ rotate: [-48, 0, 0],
72
+ op: 'intersect',
73
+ },
74
+ bustCut(f),
75
+ ],
76
+ color: shade(c.leather, 0.65),
77
+ blend: 0.004,
78
+ mirror: false,
79
+ },
80
+ {
81
+ group: [
82
+ { group: torsoForms(f, 0.105) },
83
+ {
84
+ shape: 'roundBox',
85
+ at: [cx + 0.3, -0.06, b + 0.32],
86
+ size: [0.3, 0.1, 0.1],
87
+ rotate: [-48, 0, 0],
88
+ op: 'intersect',
89
+ },
90
+ {
91
+ shape: 'roundBox',
92
+ at: [cx + 0.4, -0.06, b + 0.32],
93
+ size: [0.16, 0.05, 0.05],
94
+ rotate: [-48, 0, 0],
95
+ op: 'subtract',
96
+ },
97
+ ],
98
+ color: c.metal,
99
+ blend: 0.003,
100
+ mirror: false,
101
+ },
102
+ // Hood worn down: a rolled rim round the neck, the bag of it lying on the upper back with
103
+ // its dark mouth open upward, and a pointed tail hanging down the spine.
104
+ {
105
+ group: [
106
+ {
107
+ shape: 'torus',
108
+ at: [cx - 0.1, 0, nz + 0.03],
109
+ size: [n + 0.12, 0.06],
110
+ rotate: [0, -28, 0],
111
+ },
112
+ {
113
+ shape: 'roundBox',
114
+ at: [cx - 1.04, 0, nz],
115
+ size: [2, 4, 4],
116
+ op: 'intersect',
117
+ blend: 0.04,
118
+ },
119
+ ],
120
+ color: hood,
121
+ blend: 0.03,
122
+ },
123
+ {
124
+ group: [
125
+ { shape: 'ellipsoid', at: [cx - 0.33, 0, nz - 0.04], size: [0.13, s * 0.5, 0.25] },
126
+ {
127
+ shape: 'cone',
128
+ at: [cx - 0.36, 0, nz - 0.12],
129
+ size: [0.1, 0.015, 0.34],
130
+ rotate: alongZ(unit([-0.35, 0, -1])),
131
+ blend: 0.05,
132
+ },
133
+ {
134
+ shape: 'ellipsoid',
135
+ at: [cx - 0.3, 0, nz + 0.12],
136
+ size: [0.08, s * 0.36, 0.1],
137
+ op: 'subtract',
138
+ blend: 0.02,
139
+ color: shade(hood, 0.35),
140
+ },
141
+ ],
142
+ color: hood,
143
+ blend: 0.04,
144
+ mirror: false,
145
+ },
146
+ // Scarf: a wrap round the neck root, a knot off-centre and two tails.
147
+ {
148
+ shape: 'torus',
149
+ at: [cx - 0.04, 0, nz + 0.17],
150
+ size: [n + 0.05, 0.065],
151
+ rotate: [0, -12, 0],
152
+ color: scarf,
153
+ blend: 0.02,
154
+ },
155
+ ];
156
+ const knot: Vec3 = [cx + n * 0.8, n + 0.04, nz + 0.13];
157
+ out.push({
158
+ shape: 'ellipsoid',
159
+ at: knot,
160
+ size: [0.05, 0.06, 0.05],
161
+ color: shade(scarf, 0.85),
162
+ blend: 0.02,
163
+ mirror: false,
164
+ });
165
+ for (const [dy, len, tw] of [
166
+ [0.3, 0.2, 0.045],
167
+ [0.6, 0.16, 0.04],
168
+ ] as const) {
169
+ const dir = unit([0.3, dy, -1]);
170
+ out.push({
171
+ shape: 'cone',
172
+ at: [knot[0] + 0.01, knot[1] + dy * 0.04, knot[2] - 0.02],
173
+ size: [tw, tw * 0.7, len],
174
+ rotate: alongZ(dir),
175
+ color: dy > 0.5 ? scarf : shade(scarf, 0.8),
176
+ blend: 0.015,
177
+ mirror: false,
178
+ });
179
+ }
180
+ return out;
181
+ }
@@ -16,31 +16,42 @@ export function hairEdits(p: HeadParams, l: HeadLayout): SculptEdit[] {
16
16
  const [rx, ry, rz] = l.cranium.r;
17
17
  const top = l.crown;
18
18
  switch (p.hair) {
19
- case 'topknot':
19
+ case 'topknot': {
20
+ // A warrior's knot: hair gathered at the back of the crown, bound by two iron rings, rising
21
+ // and then falling back in a thick tapering tail to the nape.
22
+ const root: Vec3 = [cx - 0.14, 0, top - 0.03];
23
+ const rise: Vec3 = [cx - 0.2, 0, top + 0.13];
24
+ const fall: Vec3 = [cx - 0.38, 0, top + 0.02];
25
+ const tip: Vec3 = [cx - rx - 0.08, 0, cz - 0.12];
26
+ const seg = (a: Vec3, b: Vec3, r0: number, r1: number): SculptEdit => ({
27
+ shape: 'cone',
28
+ at: a,
29
+ size: [r0, r1, Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2])],
30
+ rotate: alongZ([b[0] - a[0], b[1] - a[1], b[2] - a[2]]),
31
+ blend: 0.03,
32
+ });
33
+ const ring = (at: Vec3, deg: number): SculptEdit => ({
34
+ shape: 'torus',
35
+ at,
36
+ size: [0.07, 0.018],
37
+ rotate: [0, deg, 0],
38
+ color: p.colors.metal,
39
+ blend: 0.004,
40
+ });
20
41
  return [
21
- { shape: 'ellipsoid', at: [cx - 0.12, 0, top - 0.02], size: [0.11, 0.08, 0.07], color: h },
22
- {
23
- shape: 'sphere',
24
- at: [cx - 0.16, 0, top + 0.07],
25
- size: 0.075,
26
- color: h,
27
- blend: 0.03,
28
- },
29
42
  {
30
- shape: 'capsule',
31
- at: [cx - 0.2, 0, top + 0.08],
32
- to: [cx - 0.42, 0, top - 0.25],
33
- size: 0.04,
43
+ group: [
44
+ seg(root, rise, 0.085, 0.075),
45
+ seg(rise, fall, 0.08, 0.07),
46
+ seg(fall, tip, 0.07, 0.015),
47
+ ],
34
48
  color: h,
35
- blend: 0.04,
36
- },
37
- {
38
- shape: 'torus',
39
- at: [cx - 0.17, 0, top + 0.02],
40
- size: [0.06, 0.018],
41
- color: p.colors.metal,
49
+ blend: 0.02,
42
50
  },
51
+ ring([root[0] - 0.02, 0, root[2] + 0.06], -15),
52
+ ring([root[0] - 0.04, 0, root[2] + 0.11], -25),
43
53
  ];
54
+ }
44
55
  case 'mohawk':
45
56
  return Array.from({ length: 6 }, (_, i): SculptEdit => {
46
57
  const a = (-20 + i * 26) * RAD;
@@ -112,20 +123,50 @@ export function hairEdits(p: HeadParams, l: HeadLayout): SculptEdit[] {
112
123
  lock(120, 30, 30, 0.05, 0),
113
124
  ];
114
125
  }
115
- case 'tufts':
116
- return Array.from({ length: 5 }, (_, i): SculptEdit => {
117
- const a = (i / 4 - 0.5) * 1.6;
118
- const base: Vec3 = [cx - 0.1 + Math.cos(a) * 0.05, Math.sin(a) * 0.12, top - 0.04];
119
- return {
120
- shape: 'cone',
121
- at: base,
122
- size: [0.025, 0.004, 0.12 + (i % 2) * 0.05],
123
- rotate: alongZ([-0.6, Math.sin(a) * 0.8, 1]),
126
+ case 'tufts': {
127
+ // Wispy strands: a few thin, curling wisps on the crown and over the ears, each a root cone
128
+ // and a finer tip cone bent back — straggly, not a brush.
129
+ const out: SculptEdit[] = [];
130
+ const spots: ReadonlyArray<readonly [number, number, number]> = [
131
+ [10, 78, 0.26],
132
+ [-25, 70, 0.22],
133
+ [40, 62, 0.22],
134
+ [150, 55, 0.2],
135
+ [115, 30, 0.2],
136
+ [100, 16, 0.18],
137
+ [-120, 40, 0.18],
138
+ ];
139
+ for (const [az, el, len] of spots) {
140
+ const a = az * RAD;
141
+ const e = el * RAD;
142
+ const n: Vec3 = [Math.cos(e) * Math.cos(a), Math.cos(e) * Math.sin(a), Math.sin(e)];
143
+ const root: Vec3 = [cx - 0.04 + rx * 0.97 * n[0], ry * 0.97 * n[1], cz + rz * 0.97 * n[2]];
144
+ const d1: Vec3 = [n[0] - 0.6, n[1] * 1.3, n[2] * 0.6 + 0.25];
145
+ const l1 = Math.hypot(...d1);
146
+ const mid: Vec3 = [
147
+ root[0] + (d1[0] / l1) * len * 0.55,
148
+ root[1] + (d1[1] / l1) * len * 0.55,
149
+ root[2] + (d1[2] / l1) * len * 0.55,
150
+ ];
151
+ const d2: Vec3 = [d1[0] - 0.5, d1[1] * 1.6, -0.9];
152
+ out.push({
153
+ group: [
154
+ { shape: 'cone', at: root, size: [0.026, 0.014, len * 0.58], rotate: alongZ(d1) },
155
+ {
156
+ shape: 'cone',
157
+ at: mid,
158
+ size: [0.015, 0.004, len * 0.6],
159
+ rotate: alongZ(d2),
160
+ blend: 0.008,
161
+ },
162
+ ],
124
163
  color: h,
125
- blend: 0.01,
126
- mirror: false,
127
- };
128
- });
164
+ blend: 0.012,
165
+ mirror: az !== 10 && az !== -25 && az !== -120,
166
+ });
167
+ }
168
+ return out;
169
+ }
129
170
  case 'braids':
130
171
  return [
131
172
  { ...capOf(l, h) },
@@ -8,6 +8,12 @@ import { type Field, type SculptEdit, sculptField, sd, sdf } from '../primitives
8
8
  import type { HeadLayout } from './head-layout.ts';
9
9
  import type { HeadParams } from './head-params.ts';
10
10
  import { creaseNormals, sharpenRim } from './head-rim.ts';
11
+ import { toned } from './head-tone.ts';
12
+
13
+ /** Cavity probe of the painted tone, head units: about one fold. */
14
+ const REACH = 0.05;
15
+ /** Default plinth colour (dark slate). */
16
+ export const PLINTH: Vec3 = [0.16, 0.15, 0.15];
11
17
 
12
18
  /** The jaw region (head units): an ellipsoid around chin and lower lip, cut flat at the mouth
13
19
  * line. Its skin seam runs along the lips, down the marionette lines and under the chin — creases
@@ -17,7 +23,7 @@ export function jawRegion(
17
23
  l: HeadLayout,
18
24
  ): { field: Field; centre: Vec3; radii: Vec3 } {
19
25
  const chin = l.chin;
20
- const reach = Math.max(p.mouth.width * 1.25, (p.tusks?.spread ?? 0) + p.mouth.width * 0.5);
26
+ const reach = Math.max(p.mouth.width * 1.08, (p.tusks?.spread ?? 0) + p.mouth.width * 0.5);
21
27
  const centre: Vec3 = [l.mouthX - 0.15, 0, l.mouthZ];
22
28
  const radii: Vec3 = [Math.max(0.27, l.mouthX - chin[0] + 0.2), reach, l.mouthZ - chin[2] + 0.1];
23
29
  const e = sd.ellipsoid(centre, radii);
@@ -56,6 +62,8 @@ export interface HeadEditSet {
56
62
  readonly ears: readonly SculptEdit[];
57
63
  readonly tusks: readonly SculptEdit[];
58
64
  readonly bust: readonly SculptEdit[];
65
+ /** The bust-cut region (≈ 0 on the cut faces), painted as bare plinth material. */
66
+ readonly bustCut?: Field;
59
67
  }
60
68
 
61
69
  /** `head` and `jaw` meshes in head units at cell size `res` (units). Each is ONE sculpt (skull +
@@ -72,7 +80,7 @@ export function headAndJaw(
72
80
  // The jaw reaches a little past the cut and sits a hair proud of the skin, so it laps over the
73
81
  // head like a lip: no gap opens along the seam where meshing leaves the two rims a cell apart.
74
82
  const lap = res * 0.75;
75
- const proud = res * 0.15;
83
+ const proud = res * 0.03;
76
84
  const ears: SculptEdit[] = set.ears.length > 0 ? [{ group: set.ears, blend: 0.02 }] : [];
77
85
  const tusks: SculptEdit[] = set.tusks.length > 0 ? [{ group: set.tusks, blend: 0.01 }] : [];
78
86
  const h = sculptField(
@@ -87,6 +95,18 @@ export function headAndJaw(
87
95
  const mouth = p.colors.mouth;
88
96
  // Only the skin-only skull is needed near the cut (paint + rim snapping): evaluated lazily.
89
97
  const skull = sculptField(set.skull, opts).field;
98
+ // The painted tone reads the uncut surface (skin + ears, skin + tusks): the cut faces' normals
99
+ // would light the jaw rim up as a pale outline.
100
+ const extra = (e: readonly SculptEdit[]): Field =>
101
+ e.length > 0
102
+ ? (
103
+ (f: Field): Field =>
104
+ (q) =>
105
+ Math.min(skull(q), f(q))
106
+ )(sculptField(e, opts).field)
107
+ : skull;
108
+ const headTone = extra(set.ears);
109
+ const jawTone = extra(set.tusks);
90
110
  const deep = -Math.max(res * 4, 0.03);
91
111
  const paint =
92
112
  (colorAt: (q: Vec3) => Vec3) =>
@@ -102,14 +122,14 @@ export function headAndJaw(
102
122
  field: h.field,
103
123
  bounds: h.bounds,
104
124
  resolution: res,
105
- color: paint(h.colorAt),
125
+ color: toned(headTone, paint(h.colorAt), { reach: REACH }),
106
126
  symmetry: 'y',
107
127
  });
108
128
  const jaw = sdf({
109
129
  field: jField,
110
130
  bounds: unionBounds(jb, tb),
111
131
  resolution: res,
112
- color: paint(jRaw.colorAt),
132
+ color: toned(jawTone, paint(jRaw.colorAt), { reach: REACH }),
113
133
  symmetry: 'y',
114
134
  });
115
135
  // Coarse cells: snapping folds triangles over, and nobody sees a seam at that distance.
@@ -122,13 +142,31 @@ export function headAndJaw(
122
142
  };
123
143
  }
124
144
 
145
+ /** A surface point lies on a cut face: on the cut boundary, facing out of the cut region. */
146
+ function onCut(cut: Field, q: Vec3, n: Vec3, res: number): boolean {
147
+ if (Math.abs(cut(q)) > res * 0.8) return false;
148
+ const e = res * 0.5;
149
+ const g: Vec3 = [
150
+ cut([q[0] + e, q[1], q[2]]) - cut([q[0] - e, q[1], q[2]]),
151
+ cut([q[0], q[1] + e, q[2]]) - cut([q[0], q[1] - e, q[2]]),
152
+ cut([q[0], q[1], q[2] + e]) - cut([q[0], q[1], q[2] - e]),
153
+ ];
154
+ const l = Math.hypot(g[0], g[1], g[2]) || 1;
155
+ return (g[0] * n[0] + g[1] * n[1] + g[2] * n[2]) / l > 0.85;
156
+ }
157
+
125
158
  export function bustMesh(p: HeadParams, set: HeadEditSet, res: number): Mesh {
126
159
  const s = sculptField(set.bust, { color: p.colors.skin, mirror: 'y' });
160
+ const tone = toned(s.field, s.colorAt, { reach: REACH * 1.4 });
161
+ const cut = set.bustCut;
162
+ const base = p.colors.plinth ?? PLINTH;
163
+ const face: Vec3 = [base[0] * 1.3, base[1] * 1.3, base[2] * 1.3];
127
164
  return sdf({
128
165
  field: s.field,
129
166
  bounds: s.bounds,
130
167
  resolution: res,
131
- color: (q) => s.colorAt(q),
168
+ // Cut faces read as the bare material of a sculpted bust, not as sliced layers.
169
+ color: cut ? (q, n) => (onCut(cut, q, n, res) ? face : tone(q)) : tone,
132
170
  symmetry: 'y',
133
171
  });
134
172
  }