@call-me-sensei/toonlab 0.1.1

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 (146) hide show
  1. package/ATTRIBUTION.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +125 -0
  4. package/package.json +97 -0
  5. package/src/character/characterRig.js +353 -0
  6. package/src/character/freestyleSwimClip.js +541 -0
  7. package/src/character/index.js +4 -0
  8. package/src/character/modelLoader.js +433 -0
  9. package/src/core/materialRoles.js +438 -0
  10. package/src/core/presetDocuments.js +110 -0
  11. package/src/core/shaderBackend.js +24 -0
  12. package/src/debrisgen/debrisFields.js +544 -0
  13. package/src/debrisgen/debrisGenerator.js +1929 -0
  14. package/src/debrisgen/debrisPalettes.js +71 -0
  15. package/src/debrisgen/debrisPhysics.js +198 -0
  16. package/src/debrisgen/debrisPresets.js +155 -0
  17. package/src/debrisgen/debrisSettings.js +333 -0
  18. package/src/debrisgen/debrisTextures.js +380 -0
  19. package/src/debrisgen/index.js +5 -0
  20. package/src/debug/fieldValues.js +95 -0
  21. package/src/debug/index.js +7 -0
  22. package/src/debug/settingsPanel.js +170 -0
  23. package/src/environment/environmentAmbientProbe.js +160 -0
  24. package/src/environment/environmentMaterialAdapter.js +336 -0
  25. package/src/environment/environmentMaterialClassifier.js +178 -0
  26. package/src/environment/environmentPlanarReflection.js +201 -0
  27. package/src/environment/environmentPresets.js +443 -0
  28. package/src/environment/environmentRigs.js +684 -0
  29. package/src/environment/environmentSettings.js +504 -0
  30. package/src/environment/environmentShaderMaterials.js +318 -0
  31. package/src/environment/environmentSunShadowPass.js +268 -0
  32. package/src/environment/environmentTextureResolver.js +161 -0
  33. package/src/environment/environmentTimeOfDay.js +167 -0
  34. package/src/environment/environmentVertexAo.js +188 -0
  35. package/src/environment/index.js +8 -0
  36. package/src/environment/scanAssetStylize.js +103 -0
  37. package/src/index.js +21 -0
  38. package/src/loaders/index.js +1 -0
  39. package/src/post/index.js +2 -0
  40. package/src/post/postProcessing.js +1087 -0
  41. package/src/rockgen/export/glbExport.js +177 -0
  42. package/src/rockgen/heightfield/heightfieldErosion.js +7 -0
  43. package/src/rockgen/heightfield/heightfieldPatch.js +183 -0
  44. package/src/rockgen/heightfield/stylizedErosionSim.js +356 -0
  45. package/src/rockgen/index.js +14 -0
  46. package/src/rockgen/mesh/meshAttributes.js +374 -0
  47. package/src/rockgen/mesh/meshDocument.js +158 -0
  48. package/src/rockgen/mesh/surfaceNets.js +329 -0
  49. package/src/rockgen/noise/cellularNoise3.js +92 -0
  50. package/src/rockgen/noise/prng.js +62 -0
  51. package/src/rockgen/noise/simplexNoise3.js +89 -0
  52. package/src/rockgen/noise/valueNoise3.js +87 -0
  53. package/src/rockgen/rockDocument.js +255 -0
  54. package/src/rockgen/rockHelpers.js +8 -0
  55. package/src/rockgen/rockgenPresets.js +559 -0
  56. package/src/rockgen/rockgenSettings.js +756 -0
  57. package/src/rockgen/sdf/fieldCompiler.js +544 -0
  58. package/src/rockgen/sdf/sculptEdits.js +66 -0
  59. package/src/rockgen/sdf/sdfModifiers.js +45 -0
  60. package/src/rockgen/sdf/sdfOps.js +37 -0
  61. package/src/rockgen/sdf/sdfPrimitives.js +88 -0
  62. package/src/shaders-tsl/anime.js +963 -0
  63. package/src/shaders-tsl/chunks/character-color.js +64 -0
  64. package/src/shaders-tsl/chunks/character-highlights.js +174 -0
  65. package/src/shaders-tsl/chunks/character-lighting.js +309 -0
  66. package/src/shaders-tsl/chunks/character-material-maps.js +146 -0
  67. package/src/shaders-tsl/chunks/character-roles.js +52 -0
  68. package/src/shaders-tsl/chunks/character-scene-lights.js +270 -0
  69. package/src/shaders-tsl/chunks/character-shadow-color.js +61 -0
  70. package/src/shaders-tsl/chunks/character-skinning.js +140 -0
  71. package/src/shaders-tsl/chunks/environment-color.js +47 -0
  72. package/src/shaders-tsl/chunks/environment-debug.js +79 -0
  73. package/src/shaders-tsl/chunks/environment-lighting.js +260 -0
  74. package/src/shaders-tsl/chunks/environment-sun-shadow.js +87 -0
  75. package/src/shaders-tsl/chunks/foliage-fog.js +60 -0
  76. package/src/shaders-tsl/chunks/pass-depth-color.js +89 -0
  77. package/src/shaders-tsl/chunks/stylized-cloud-shadow.js +70 -0
  78. package/src/shaders-tsl/chunks/water-color.js +190 -0
  79. package/src/shaders-tsl/chunks/water-common.js +124 -0
  80. package/src/shaders-tsl/chunks/water-foam.js +93 -0
  81. package/src/shaders-tsl/chunks/water-lighting.js +116 -0
  82. package/src/shaders-tsl/chunks/water-ripple.js +64 -0
  83. package/src/shaders-tsl/chunks/water-waves.js +100 -0
  84. package/src/shaders-tsl/environment-ao-overlay.js +60 -0
  85. package/src/shaders-tsl/environment.js +680 -0
  86. package/src/shaders-tsl/flower.js +267 -0
  87. package/src/shaders-tsl/grass.js +193 -0
  88. package/src/shaders-tsl/post-composite.js +461 -0
  89. package/src/shaders-tsl/sky.js +175 -0
  90. package/src/shaders-tsl/tree-leaf.js +264 -0
  91. package/src/shaders-tsl/water-breaker.js +423 -0
  92. package/src/shaders-tsl/water-kelp.js +171 -0
  93. package/src/shaders-tsl/water-rain.js +107 -0
  94. package/src/shaders-tsl/water-simulation.js +140 -0
  95. package/src/shaders-tsl/water-splash.js +298 -0
  96. package/src/shaders-tsl/water.js +509 -0
  97. package/src/sky/index.js +2 -0
  98. package/src/sky/stylizedSky.js +341 -0
  99. package/src/toon/characterRenderPasses.js +646 -0
  100. package/src/toon/index.js +4 -0
  101. package/src/toon/settings/alphaSettings.js +170 -0
  102. package/src/toon/settings/averageShadowSettings.js +181 -0
  103. package/src/toon/settings/baseTextureSettings.js +133 -0
  104. package/src/toon/settings/celShadeSettings.js +96 -0
  105. package/src/toon/settings/contactShadowSettings.js +114 -0
  106. package/src/toon/settings/eyeHighlightSettings.js +187 -0
  107. package/src/toon/settings/faceLightingSettings.js +135 -0
  108. package/src/toon/settings/furSettings.js +112 -0
  109. package/src/toon/settings/glitterSettings.js +102 -0
  110. package/src/toon/settings/hairHighlightSettings.js +325 -0
  111. package/src/toon/settings/indirectLightSettings.js +244 -0
  112. package/src/toon/settings/localLightSettings.js +176 -0
  113. package/src/toon/settings/materialMapSettings.js +299 -0
  114. package/src/toon/settings/outlineSettings.js +263 -0
  115. package/src/toon/settings/perspectiveRemovalSettings.js +60 -0
  116. package/src/toon/settings/rimLightSettings.js +268 -0
  117. package/src/toon/settings/sceneShadowSettings.js +147 -0
  118. package/src/toon/settings/selfShadowSettings.js +221 -0
  119. package/src/toon/settings/shadowColorSettings.js +238 -0
  120. package/src/toon/settings/skinToneSettings.js +150 -0
  121. package/src/toon/settings/specularSettings.js +314 -0
  122. package/src/toon/settings/stickerSettings.js +102 -0
  123. package/src/toon/toonMaterialAdapter.js +2235 -0
  124. package/src/toon/toonSettings.js +860 -0
  125. package/src/vegetation/flowerSpecies.js +260 -0
  126. package/src/vegetation/index.js +10 -0
  127. package/src/vegetation/stylizedBush.js +136 -0
  128. package/src/vegetation/stylizedFlower.js +213 -0
  129. package/src/vegetation/stylizedFlowers.js +297 -0
  130. package/src/vegetation/stylizedGrass.js +491 -0
  131. package/src/vegetation/stylizedTree.js +2449 -0
  132. package/src/vegetation/stylizedTreeFoliage.js +691 -0
  133. package/src/vegetation/treeExport.js +300 -0
  134. package/src/vegetation/treeRecipe.js +647 -0
  135. package/src/water/index.js +2 -0
  136. package/src/water/water.js +20 -0
  137. package/src/water/waterBreakerSystem.js +591 -0
  138. package/src/water/waterInteraction.js +193 -0
  139. package/src/water/waterMaterial.js +263 -0
  140. package/src/water/waterRain.js +136 -0
  141. package/src/water/waterRippleSimulation.js +227 -0
  142. package/src/water/waterScenePasses.js +415 -0
  143. package/src/water/waterSettings.js +1138 -0
  144. package/src/water/waterSplashSystem.js +251 -0
  145. package/src/water/waterSurface.js +475 -0
  146. package/src/water/waterVegetation.js +83 -0
package/ATTRIBUTION.md ADDED
@@ -0,0 +1,40 @@
1
+ # Asset attribution
2
+
3
+ Code is MIT-licensed by Hyperbond Studio PTE. LTD. (see LICENSE). The assets bundled in this repository:
4
+
5
+ ## Bundled assets
6
+
7
+ | Asset | Location | License | Source |
8
+ |---|---|---|---|
9
+ | Mannequin character (45 embedded animation clips) | `public/characters/mannequin.glb` | CC0 1.0 | [Quaternius — Universal Animation Library](https://quaternius.com/packs/universalanimationlibrary.html), recolored to neutral grays. See `public/characters/LICENSE.md`. |
10
+ | Landscape textures (grassy-land, land, mountain, rock, sand, tree-trunk) | `labs/shared/textures/` | CC0 1.0 | First-party (created by the toonlab authors), dedicated to the public domain. |
11
+
12
+ Everything else that renders — water, sky, grass, flowers, trees, foliage,
13
+ splashes, post-processing — is procedural code, no asset files.
14
+
15
+ ## Not bundled (bring your own)
16
+
17
+ The labs can additionally load content from a gitignored `assets-local/`
18
+ folder that is **not** part of this repository:
19
+
20
+ - **Mixamo animation clips** (`assets-local/animations/*.fbx`) — the retarget
21
+ pipeline plays Adobe Mixamo clips on any humanoid model. Download clips
22
+ with your own Adobe account from [mixamo.com](https://www.mixamo.com) and
23
+ drop them in; Adobe's terms do not permit us to redistribute the files.
24
+ Without them, models fall back to their own embedded clips (the bundled
25
+ mannequin covers idle/walk/run/jump/swim/tread natively).
26
+ - **Your models** (`assets-local/models/`) — PMX/VRM/GLB/FBX/OBJ characters
27
+ for testing; or pass any hosted URL via the HUD's Model URL field /
28
+ `?model=`. Make sure you have the rights to any model you load.
29
+ - **Scanned props/environments** (`assets-local/environments/`) — licensed
30
+ packs (Fab/Megascans etc.) can be dropped in for the demo scenes;
31
+ the scenes render procedural stand-ins when they're absent.
32
+
33
+ ## Recommended free sources
34
+
35
+ - [Quaternius](https://quaternius.com) — CC0 models + the Universal
36
+ Animation Library (v1 and v2 free tiers).
37
+ - [Poly Haven](https://polyhaven.com) — CC0 textures/HDRIs/models.
38
+ - [Kenney](https://kenney.nl) — CC0 game assets.
39
+ - [OpenGameArt](https://opengameart.org) — filter by CC0/CC-BY.
40
+ - [VRoid Hub](https://hub.vroid.com) — VRM avatars (check per-model terms).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hyperbond Studio PTE. LTD.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # ToonLab by Call Me Sensei
2
+
3
+ ToonLab by Call Me Sensei is a stylized anime-style game starter kit and
4
+ runtime library for [Three.js](https://threejs.org/). Its goal is to simplify
5
+ Three.js game development: you get a better-looking anime/stylized game
6
+ without having to develop shaders yourself, and procedurally generated assets
7
+ (trees, rocks, grass, water, sky) reduce content complexity so you can focus
8
+ on your core game logic instead. Clone it and a toon-shaded character is
9
+ walking and swimming through a fully stylized world: cel-shaded characters,
10
+ painterly environments, interactive water, procedural vegetation and sky,
11
+ post-processing, and a schema-driven tuning panel. Use it as a starter kit,
12
+ or import the pieces you need as a library (`@call-me-sensei/toonlab` on npm,
13
+ subpath exports per cluster).
14
+
15
+ ## Quickstart
16
+
17
+ ```bash
18
+ git clone https://github.com/call-me-sensei/toonlab.git && cd toonlab
19
+ npm install
20
+ npm run dev
21
+ ```
22
+
23
+ Vite serves the labs at `http://localhost:5175`. See
24
+ [Getting started](docs/getting-started.md) for a tour.
25
+
26
+ ## The labs
27
+
28
+ Runnable demos in `labs/` (not published to npm). Switch between them with
29
+ the HUD Scene select:
30
+
31
+ - **Shader Lab** (`/`) — character + environment shader tuning: every toon
32
+ and environment setting as a live control, preset export/import, debug
33
+ views.
34
+ - **Playground** (`/playground/`) — third-person character controller
35
+ in gameplay scenes: calm lake, river crossing, ocean beach with breaking
36
+ waves, and an indoor room. Walk, jump, swim, splash.
37
+ - **Rock Lab** (`/rock-lab/`) — procedural stylized rocks, cliffs,
38
+ heightfields, sculpt edits, and GLB export.
39
+ - **Tree Lab** (`/tree-lab/`) — procedural stylized trees,
40
+ flowers, sketches, recipes, and GLB export.
41
+
42
+ Every URL parameter has a HUD control, lab state persists per lab in
43
+ `localStorage`, and **Reset Lab** clears it. Point any lab at your own model
44
+ with the Model URL input or `?model=` — see
45
+ [Characters](docs/characters.md).
46
+
47
+ ## What's inside
48
+
49
+ | Cluster | Import | What you get |
50
+ |---|---|---|
51
+ | Toon character shading | `@call-me-sensei/toonlab/toon` | Modern anime character shader: cel bands with art-directed face lighting, skin-tone shadow management, shadow-color HSV control, scene/self/contact shadows, average-shadow smoothing, rim light (fresnel or screen-space depth), stylized + anisotropic hair highlights, eye catchlights, role-aware specular, source map routing (normal/AO/emissive/MatCap/ramp/detail), inverted-hull outlines, glitter, stickers, perspective removal, shell fur, dither fades — 23 settings groups, all preset-serializable. [Docs](docs/toon-shading.md) |
52
+ | Environment shading | `@call-me-sensei/toonlab/environment` | Modern anime-style scene shader for texture packs, standard glTF, and untextured scenes: material-role classification, wrapped lighting, packed-map hints, window cutouts, sun/lamp rigs, time-of-day, six-direction ambient probe, planar floor reflections, BVH vertex-AO baking, height fog, cloud shadows. [Docs](docs/environment.md) |
53
+ | Water | `@call-me-sensei/toonlab/water` | Fully procedural interactive water: Gerstner wave stack with a calm→storm dial, wave sets, plunging breakers you can surf, three-stop absorption color, refraction/caustics/foam, GPU ripple sim, splashes, wakes, rain, kelp, underwater view — with a CPU mirror of the whole spectrum for buoyancy. [Docs](docs/water.md) |
54
+ | Vegetation | `@call-me-sensei/toonlab/vegetation` | Instanced grass and flower fields (wind, push-away, scene + cloud shadows, backlit translucency) and procedural stylized trees with a serializable recipe system. [Docs](docs/vegetation-sky.md) |
55
+ | Sky | `@call-me-sensei/toonlab/sky` | Procedural gradient/sun/painterly-cloud/star dome that also shows up in water reflections. [Docs](docs/vegetation-sky.md) |
56
+ | Post-processing | `@call-me-sensei/toonlab/post` | Optional single-pipeline compositor: character-aware bloom, color grade, LUT, vignette, screen outline, depth cue — schema-driven, preset-serializable. [Docs](docs/post-processing.md) |
57
+ | Character pipeline | `@call-me-sensei/toonlab/character` | Bone-role adapters for VRM/MMD/Mixamo/Rigify rigs, native-clip conventions, Mixamo retarget helpers, and a procedural freestyle swim clip. [Docs](docs/characters.md) |
58
+ | Model loaders | `@call-me-sensei/toonlab/loaders` | Optional GLB/glTF, VRM 0+1, PMX/PMD, FBX, OBJ, and text-USDZ loading helpers. Kept off the root import so apps that do not load models avoid loader dependencies. [Docs](docs/characters.md) |
59
+ | Debug panel | `@call-me-sensei/toonlab/debug` | One-line schema-driven tuning GUI for any settings module — the same panel the labs use. [Docs](docs/debug-panel.md) |
60
+
61
+ Zero texture assets in the library: water, sky, grass, flowers, trees, and
62
+ splashes are all procedural. The one bundled model is a CC0 mannequin with 45
63
+ embedded animation clips.
64
+
65
+ ## Library usage
66
+
67
+ ```js
68
+ import { applyToonShader, createToonSettings } from '@call-me-sensei/toonlab/toon';
69
+
70
+ const settings = createToonSettings({
71
+ preset: 'default',
72
+ skinTone: {
73
+ skinShadowBrightness: 0.94,
74
+ },
75
+ });
76
+
77
+ applyToonShader(characterRoot, { settings });
78
+ ```
79
+
80
+ ```js
81
+ import { WaterSurface } from '@call-me-sensei/toonlab/water';
82
+ import { StylizedSky } from '@call-me-sensei/toonlab/sky';
83
+
84
+ const water = new WaterSurface({ width: 200, depth: 200, preset: 'lake' });
85
+ scene.add(water);
86
+ const sky = new StylizedSky();
87
+ scene.add(sky);
88
+
89
+ // per frame, before renderer.render(scene, camera):
90
+ water.update(renderer, scene, camera, delta);
91
+ sky.update(delta, camera);
92
+ ```
93
+
94
+ Inside this repo the labs import from `../../src/...`; the `@call-me-sensei/toonlab/...`
95
+ specifiers are what you use once the package is installed from npm. The
96
+ shader clusters are TSL/NodeMaterial modules for Three's WebGPU renderer
97
+ stack, with WebGL2 fallback through the same TSL path.
98
+
99
+ ## Documentation
100
+
101
+ - [Getting started](docs/getting-started.md) — clone, run, tour the labs,
102
+ load your own models.
103
+ - [Toon character shading](docs/toon-shading.md)
104
+ - [Environment shading](docs/environment.md)
105
+ - [Water](docs/water.md)
106
+ - [Vegetation and sky](docs/vegetation-sky.md)
107
+ - [Post-processing](docs/post-processing.md)
108
+ - [Characters and animation](docs/characters.md)
109
+ - [Debug panel](docs/debug-panel.md)
110
+ - [Settings reference](docs/settings-reference.md) — every tunable field,
111
+ generated from the schemas (`node scripts/generate-settings-reference.mjs`).
112
+ - [Shader constants](docs/shader-constants.md) — the deliberately unexposed
113
+ constants and where they live.
114
+
115
+ ## AI coding agents
116
+
117
+ The GitHub repo includes downloadable runtime-usage guidance for Codex, Claude
118
+ Code, Cursor, and other coding agents under `agents/`. These files help
119
+ developers use ToonLab in their own apps and are not part of the npm package.
120
+
121
+ ## License
122
+
123
+ Code is [MIT](LICENSE), copyright Hyperbond Studio PTE. LTD. Bundled assets are CC0 — see
124
+ [ATTRIBUTION.md](ATTRIBUTION.md) for credits and for the bring-your-own
125
+ conventions (Mixamo clips, your own models, licensed scan packs).
package/package.json ADDED
@@ -0,0 +1,97 @@
1
+ {
2
+ "name": "@call-me-sensei/toonlab",
3
+ "version": "0.1.1",
4
+ "description": "Stylized (anime/toon) game starter kit for Three.js: character toon shading, environment shading, water, vegetation, sky, and post-processing",
5
+ "license": "MIT",
6
+ "author": "Hyperbond Studio PTE. LTD. (Call Me Sensei)",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/call-me-sensei/toonlab.git"
10
+ },
11
+ "homepage": "https://github.com/call-me-sensei/toonlab#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/call-me-sensei/toonlab/issues"
14
+ },
15
+ "keywords": [
16
+ "three",
17
+ "threejs",
18
+ "toon",
19
+ "cel-shading",
20
+ "anime",
21
+ "shader",
22
+ "water",
23
+ "game-starter"
24
+ ],
25
+ "type": "module",
26
+ "main": "./src/index.js",
27
+ "module": "./src/index.js",
28
+ "files": [
29
+ "src",
30
+ "README.md",
31
+ "LICENSE",
32
+ "ATTRIBUTION.md"
33
+ ],
34
+ "exports": {
35
+ ".": "./src/index.js",
36
+ "./toon": "./src/toon/index.js",
37
+ "./environment": "./src/environment/index.js",
38
+ "./water": "./src/water/index.js",
39
+ "./vegetation": "./src/vegetation/index.js",
40
+ "./sky": "./src/sky/index.js",
41
+ "./post": "./src/post/index.js",
42
+ "./rockgen": "./src/rockgen/index.js",
43
+ "./debrisgen": "./src/debrisgen/index.js",
44
+ "./character": "./src/character/index.js",
45
+ "./loaders": "./src/loaders/index.js",
46
+ "./debug": "./src/debug/index.js",
47
+ "./toon-settings": "./src/toon/toonSettings.js",
48
+ "./water-settings": "./src/water/waterSettings.js",
49
+ "./grass": "./src/vegetation/stylizedGrass.js",
50
+ "./post-processing": "./src/post/postProcessing.js"
51
+ },
52
+ "scripts": {
53
+ "assets:characters": "node scripts/generate-local-character-assets.mjs",
54
+ "assets:environments": "node scripts/generate-local-environment-assets.mjs",
55
+ "assets:local": "npm run assets:characters && npm run assets:environments",
56
+ "dev": "vite",
57
+ "build": "vite build",
58
+ "baseline:compare": "node scripts/compare-baselines.mjs",
59
+ "preview": "vite preview",
60
+ "baseline:capture": "node scripts/baseline-capture.mjs",
61
+ "verify:rockgen": "node scripts/verify-rockgen.mjs",
62
+ "visual:check": "node scripts/visual-check.mjs"
63
+ },
64
+ "peerDependencies": {
65
+ "@pixiv/three-vrm": "^3.5.4",
66
+ "three-mesh-bvh": "^0.8.3",
67
+ "three-stdlib": "^2.36.1",
68
+ "three": "^0.185.1"
69
+ },
70
+ "peerDependenciesMeta": {
71
+ "@pixiv/three-vrm": {
72
+ "optional": true
73
+ },
74
+ "three-mesh-bvh": {
75
+ "optional": true
76
+ },
77
+ "three-stdlib": {
78
+ "optional": true
79
+ }
80
+ },
81
+ "devDependencies": {
82
+ "@pixiv/three-vrm": "^3.5.4",
83
+ "@react-three/drei": "^10.7.7",
84
+ "@react-three/fiber": "^9.6.1",
85
+ "@react-three/rapier": "^2.2.0",
86
+ "@vitejs/plugin-react": "^4.7.0",
87
+ "ecctrl": "^1.0.97",
88
+ "lucide-react": "^1.23.0",
89
+ "playwright": "^1.60.0",
90
+ "react": "^19.2.7",
91
+ "react-dom": "^19.2.7",
92
+ "three": "^0.185.1",
93
+ "three-mesh-bvh": "^0.8.3",
94
+ "three-stdlib": "^2.36.1",
95
+ "vite": "^5.0.0"
96
+ }
97
+ }
@@ -0,0 +1,353 @@
1
+ // Canonical humanoid bone roles and per-convention name adapters.
2
+ //
3
+ // The retarget pipeline in ecctrlMain.jsx does a world-space bake that only
4
+ // needs to know WHICH target bone plays WHICH humanoid role — everything else
5
+ // (rest-pose differences, hierarchy mismatches) is solved numerically. This
6
+ // module owns that role knowledge so the pipeline itself stays rig-agnostic:
7
+ // roles follow the VRM humanoid naming (the only spec'd humanoid standard in
8
+ // the glTF world), and each supported convention (VRM, MMD/PMX, Mixamo-named
9
+ // skeletons) contributes a small name table mapping onto those roles.
10
+ //
11
+ // Animation sources are Mixamo FBX clips, so each role also carries its
12
+ // Mixamo bone name; a resolved rig is ultimately a pair of maps between
13
+ // target bone names and Mixamo source names.
14
+
15
+ // Mixamo bone per VRM humanoid role. Also the source-side chain map used for
16
+ // rest-pose direction corrections lives on these names (MIXAMO_CHAIN_CHILD).
17
+ const MIXAMO_BONE_BY_ROLE = Object.freeze({
18
+ hips: 'mixamorigHips',
19
+ spine: 'mixamorigSpine',
20
+ chest: 'mixamorigSpine1',
21
+ upperChest: 'mixamorigSpine2',
22
+ neck: 'mixamorigNeck',
23
+ head: 'mixamorigHead',
24
+
25
+ leftShoulder: 'mixamorigLeftShoulder',
26
+ leftUpperArm: 'mixamorigLeftArm',
27
+ leftLowerArm: 'mixamorigLeftForeArm',
28
+ leftHand: 'mixamorigLeftHand',
29
+ rightShoulder: 'mixamorigRightShoulder',
30
+ rightUpperArm: 'mixamorigRightArm',
31
+ rightLowerArm: 'mixamorigRightForeArm',
32
+ rightHand: 'mixamorigRightHand',
33
+
34
+ leftThumbMetacarpal: 'mixamorigLeftHandThumb1',
35
+ leftThumbProximal: 'mixamorigLeftHandThumb2',
36
+ leftThumbDistal: 'mixamorigLeftHandThumb3',
37
+ leftIndexProximal: 'mixamorigLeftHandIndex1',
38
+ leftIndexIntermediate: 'mixamorigLeftHandIndex2',
39
+ leftIndexDistal: 'mixamorigLeftHandIndex3',
40
+ leftMiddleProximal: 'mixamorigLeftHandMiddle1',
41
+ leftMiddleIntermediate: 'mixamorigLeftHandMiddle2',
42
+ leftMiddleDistal: 'mixamorigLeftHandMiddle3',
43
+ leftRingProximal: 'mixamorigLeftHandRing1',
44
+ leftRingIntermediate: 'mixamorigLeftHandRing2',
45
+ leftRingDistal: 'mixamorigLeftHandRing3',
46
+ leftLittleProximal: 'mixamorigLeftHandPinky1',
47
+ leftLittleIntermediate: 'mixamorigLeftHandPinky2',
48
+ leftLittleDistal: 'mixamorigLeftHandPinky3',
49
+
50
+ rightThumbMetacarpal: 'mixamorigRightHandThumb1',
51
+ rightThumbProximal: 'mixamorigRightHandThumb2',
52
+ rightThumbDistal: 'mixamorigRightHandThumb3',
53
+ rightIndexProximal: 'mixamorigRightHandIndex1',
54
+ rightIndexIntermediate: 'mixamorigRightHandIndex2',
55
+ rightIndexDistal: 'mixamorigRightHandIndex3',
56
+ rightMiddleProximal: 'mixamorigRightHandMiddle1',
57
+ rightMiddleIntermediate: 'mixamorigRightHandMiddle2',
58
+ rightMiddleDistal: 'mixamorigRightHandMiddle3',
59
+ rightRingProximal: 'mixamorigRightHandRing1',
60
+ rightRingIntermediate: 'mixamorigRightHandRing2',
61
+ rightRingDistal: 'mixamorigRightHandRing3',
62
+ rightLittleProximal: 'mixamorigRightHandPinky1',
63
+ rightLittleIntermediate: 'mixamorigRightHandPinky2',
64
+ rightLittleDistal: 'mixamorigRightHandPinky3',
65
+
66
+ leftUpperLeg: 'mixamorigLeftUpLeg',
67
+ leftLowerLeg: 'mixamorigLeftLeg',
68
+ leftFoot: 'mixamorigLeftFoot',
69
+ leftToes: 'mixamorigLeftToeBase',
70
+ rightUpperLeg: 'mixamorigRightUpLeg',
71
+ rightLowerLeg: 'mixamorigRightLeg',
72
+ rightFoot: 'mixamorigRightFoot',
73
+ rightToes: 'mixamorigRightToeBase',
74
+ });
75
+
76
+ export const HUMANOID_ROLES = Object.freeze(Object.keys(MIXAMO_BONE_BY_ROLE));
77
+
78
+ // Source-side chain children for rest-pose direction corrections (Mixamo rigs
79
+ // are T-pose; targets are often A-pose). Keyed by Mixamo names because the
80
+ // correction compares source bone directions against target bone directions.
81
+ export const MIXAMO_CHAIN_CHILD = Object.freeze({
82
+ mixamorigLeftShoulder: 'mixamorigLeftArm',
83
+ mixamorigLeftArm: 'mixamorigLeftForeArm',
84
+ mixamorigLeftForeArm: 'mixamorigLeftHand',
85
+ mixamorigLeftHand: 'mixamorigLeftHandMiddle1',
86
+ mixamorigRightShoulder: 'mixamorigRightArm',
87
+ mixamorigRightArm: 'mixamorigRightForeArm',
88
+ mixamorigRightForeArm: 'mixamorigRightHand',
89
+ mixamorigRightHand: 'mixamorigRightHandMiddle1',
90
+ mixamorigLeftUpLeg: 'mixamorigLeftLeg',
91
+ mixamorigLeftLeg: 'mixamorigLeftFoot',
92
+ mixamorigLeftFoot: 'mixamorigLeftToeBase',
93
+ mixamorigRightUpLeg: 'mixamorigRightLeg',
94
+ mixamorigRightLeg: 'mixamorigRightFoot',
95
+ mixamorigRightFoot: 'mixamorigRightToeBase',
96
+ });
97
+
98
+ // MMD/PMX bone names per role. Multiple entries per role are all animated:
99
+ // many rigs (incl. many anime-game models) bind the leg skin weights to the "D"
100
+ // deform bones, which only follow the FK bones through MMD's grant system
101
+ // (needs MMDAnimationHelper) — animating both moves every variant. The LAST
102
+ // existing entry is the preferred one for chain/inverse lookups, matching the
103
+ // deform-bone preference the old hardcoded tables had.
104
+ const MMD_BONES_BY_ROLE = Object.freeze({
105
+ hips: ['下半身'],
106
+ spine: ['上半身'],
107
+ chest: ['上半身3'],
108
+ upperChest: ['上半身2'],
109
+ neck: ['首'],
110
+ head: ['頭'],
111
+
112
+ leftShoulder: ['左肩'],
113
+ leftUpperArm: ['左腕'],
114
+ leftLowerArm: ['左ひじ'],
115
+ leftHand: ['左手首'],
116
+ rightShoulder: ['右肩'],
117
+ rightUpperArm: ['右腕'],
118
+ rightLowerArm: ['右ひじ'],
119
+ rightHand: ['右手首'],
120
+
121
+ leftThumbMetacarpal: ['左親指0'],
122
+ leftThumbProximal: ['左親指1'],
123
+ leftThumbDistal: ['左親指2'],
124
+ leftIndexProximal: ['左人指1'],
125
+ leftIndexIntermediate: ['左人指2'],
126
+ leftIndexDistal: ['左人指3'],
127
+ leftMiddleProximal: ['左中指1'],
128
+ leftMiddleIntermediate: ['左中指2'],
129
+ leftMiddleDistal: ['左中指3'],
130
+ leftRingProximal: ['左薬指1'],
131
+ leftRingIntermediate: ['左薬指2'],
132
+ leftRingDistal: ['左薬指3'],
133
+ leftLittleProximal: ['左小指1'],
134
+ leftLittleIntermediate: ['左小指2'],
135
+ leftLittleDistal: ['左小指3'],
136
+
137
+ rightThumbMetacarpal: ['右親指0'],
138
+ rightThumbProximal: ['右親指1'],
139
+ rightThumbDistal: ['右親指2'],
140
+ rightIndexProximal: ['右人指1'],
141
+ rightIndexIntermediate: ['右人指2'],
142
+ rightIndexDistal: ['右人指3'],
143
+ rightMiddleProximal: ['右中指1'],
144
+ rightMiddleIntermediate: ['右中指2'],
145
+ rightMiddleDistal: ['右中指3'],
146
+ rightRingProximal: ['右薬指1'],
147
+ rightRingIntermediate: ['右薬指2'],
148
+ rightRingDistal: ['右薬指3'],
149
+ rightLittleProximal: ['右小指1'],
150
+ rightLittleIntermediate: ['右小指2'],
151
+ rightLittleDistal: ['右小指3'],
152
+
153
+ leftUpperLeg: ['左足', '左足D'],
154
+ leftLowerLeg: ['左ひざ', '左ひざD'],
155
+ leftFoot: ['左足首', '左足首D'],
156
+ leftToes: ['左つま先', '左足先EX'],
157
+ rightUpperLeg: ['右足', '右足D'],
158
+ rightLowerLeg: ['右ひざ', '右ひざD'],
159
+ rightFoot: ['右足首', '右足首D'],
160
+ rightToes: ['右つま先', '右足先EX'],
161
+ });
162
+
163
+ // Blender Rigify deform-bone names (the bundled mannequin and other
164
+ // Rigify-exported GLBs). GLTFLoader sanitizes node names on load (dots and
165
+ // other reserved characters are stripped: "DEF-upper_arm.L" arrives as
166
+ // "DEF-upper_armL"), so this table is matched loosely — see looseBoneName().
167
+ const RIGIFY_BONES_BY_ROLE = Object.freeze({
168
+ hips: ['DEF-hips'],
169
+ spine: ['DEF-spine.001'],
170
+ chest: ['DEF-spine.002'],
171
+ upperChest: ['DEF-spine.003'],
172
+ neck: ['DEF-neck'],
173
+ head: ['DEF-head'],
174
+
175
+ leftShoulder: ['DEF-shoulder.L'],
176
+ leftUpperArm: ['DEF-upper_arm.L'],
177
+ leftLowerArm: ['DEF-forearm.L'],
178
+ leftHand: ['DEF-hand.L'],
179
+ rightShoulder: ['DEF-shoulder.R'],
180
+ rightUpperArm: ['DEF-upper_arm.R'],
181
+ rightLowerArm: ['DEF-forearm.R'],
182
+ rightHand: ['DEF-hand.R'],
183
+
184
+ leftThumbMetacarpal: ['DEF-thumb.01.L'],
185
+ leftThumbProximal: ['DEF-thumb.02.L'],
186
+ leftThumbDistal: ['DEF-thumb.03.L'],
187
+ leftIndexProximal: ['DEF-f_index.01.L'],
188
+ leftIndexIntermediate: ['DEF-f_index.02.L'],
189
+ leftIndexDistal: ['DEF-f_index.03.L'],
190
+ leftMiddleProximal: ['DEF-f_middle.01.L'],
191
+ leftMiddleIntermediate: ['DEF-f_middle.02.L'],
192
+ leftMiddleDistal: ['DEF-f_middle.03.L'],
193
+ leftRingProximal: ['DEF-f_ring.01.L'],
194
+ leftRingIntermediate: ['DEF-f_ring.02.L'],
195
+ leftRingDistal: ['DEF-f_ring.03.L'],
196
+ leftLittleProximal: ['DEF-f_pinky.01.L'],
197
+ leftLittleIntermediate: ['DEF-f_pinky.02.L'],
198
+ leftLittleDistal: ['DEF-f_pinky.03.L'],
199
+
200
+ rightThumbMetacarpal: ['DEF-thumb.01.R'],
201
+ rightThumbProximal: ['DEF-thumb.02.R'],
202
+ rightThumbDistal: ['DEF-thumb.03.R'],
203
+ rightIndexProximal: ['DEF-f_index.01.R'],
204
+ rightIndexIntermediate: ['DEF-f_index.02.R'],
205
+ rightIndexDistal: ['DEF-f_index.03.R'],
206
+ rightMiddleProximal: ['DEF-f_middle.01.R'],
207
+ rightMiddleIntermediate: ['DEF-f_middle.02.R'],
208
+ rightMiddleDistal: ['DEF-f_middle.03.R'],
209
+ rightRingProximal: ['DEF-f_ring.01.R'],
210
+ rightRingIntermediate: ['DEF-f_ring.02.R'],
211
+ rightRingDistal: ['DEF-f_ring.03.R'],
212
+ rightLittleProximal: ['DEF-f_pinky.01.R'],
213
+ rightLittleIntermediate: ['DEF-f_pinky.02.R'],
214
+ rightLittleDistal: ['DEF-f_pinky.03.R'],
215
+
216
+ leftUpperLeg: ['DEF-thigh.L'],
217
+ leftLowerLeg: ['DEF-shin.L'],
218
+ leftFoot: ['DEF-foot.L'],
219
+ leftToes: ['DEF-toe.L'],
220
+ rightUpperLeg: ['DEF-thigh.R'],
221
+ rightLowerLeg: ['DEF-shin.R'],
222
+ rightFoot: ['DEF-foot.R'],
223
+ rightToes: ['DEF-toe.R'],
224
+ });
225
+
226
+ // MMD's translation carrier: hip sway/bob is baked onto センター so any stance
227
+ // stays balanced over the capsule. Non-MMD rigs carry it on the hips bone.
228
+ const MMD_CENTER_BONE = 'センター';
229
+
230
+ function looseBoneName(name) {
231
+ return String(name || '').replace(/[^0-9A-Za-z_\-぀-ヿ一-鿿!-⦆]/g, '');
232
+ }
233
+
234
+ export function normalizeMixamoBoneName(name) {
235
+ return String(name || '')
236
+ .replace(/^mixamorig:/, 'mixamorig')
237
+ .replace(/:/g, '');
238
+ }
239
+
240
+ function buildRig(type, entries, { hipCarrierName = null } = {}) {
241
+ // entries: [{ targetName, mixamoName }] in preference order per mixamo name
242
+ // (later entries override earlier ones in the inverse map).
243
+ const targetToMixamo = new Map();
244
+ const mixamoToTarget = new Map();
245
+ for (const { targetName, mixamoName } of entries) {
246
+ if (!targetToMixamo.has(targetName)) targetToMixamo.set(targetName, mixamoName);
247
+ mixamoToTarget.set(mixamoName, targetName);
248
+ }
249
+ if (targetToMixamo.size === 0) return null;
250
+
251
+ return {
252
+ type,
253
+ targetToMixamo,
254
+ mixamoToTarget,
255
+ hipCarrierName: hipCarrierName || mixamoToTarget.get(MIXAMO_BONE_BY_ROLE.hips) || null,
256
+ };
257
+ }
258
+
259
+ function resolveVrmRig(vrm) {
260
+ const humanoid = vrm?.humanoid;
261
+ if (!humanoid?.getRawBoneNode) return null;
262
+
263
+ const entries = [];
264
+ for (const [role, mixamoName] of Object.entries(MIXAMO_BONE_BY_ROLE)) {
265
+ const node = humanoid.getRawBoneNode(role);
266
+ if (node?.name) entries.push({ targetName: node.name, mixamoName });
267
+ }
268
+
269
+ return buildRig('vrm', entries);
270
+ }
271
+
272
+ function resolveMmdRig(boneNames) {
273
+ const entries = [];
274
+ for (const [role, targetNames] of Object.entries(MMD_BONES_BY_ROLE)) {
275
+ const mixamoName = MIXAMO_BONE_BY_ROLE[role];
276
+ for (const targetName of targetNames) {
277
+ if (boneNames.has(targetName)) entries.push({ targetName, mixamoName });
278
+ }
279
+ }
280
+
281
+ return buildRig('mmd', entries, {
282
+ hipCarrierName: boneNames.has(MMD_CENTER_BONE) ? MMD_CENTER_BONE : null,
283
+ });
284
+ }
285
+
286
+ function resolveRigifyRig(boneNames) {
287
+ const actualByLooseName = new Map();
288
+ for (const boneName of boneNames) actualByLooseName.set(looseBoneName(boneName), boneName);
289
+
290
+ const entries = [];
291
+ for (const [role, candidates] of Object.entries(RIGIFY_BONES_BY_ROLE)) {
292
+ const mixamoName = MIXAMO_BONE_BY_ROLE[role];
293
+ for (const candidate of candidates) {
294
+ const targetName = actualByLooseName.get(looseBoneName(candidate));
295
+ if (targetName) entries.push({ targetName, mixamoName });
296
+ }
297
+ }
298
+
299
+ return buildRig('rigify', entries);
300
+ }
301
+
302
+ function resolveMixamoNamedRig(boneNames) {
303
+ const mixamoNames = new Set(Object.values(MIXAMO_BONE_BY_ROLE));
304
+ const entries = [];
305
+ for (const boneName of boneNames) {
306
+ const normalized = normalizeMixamoBoneName(boneName);
307
+ if (mixamoNames.has(normalized)) entries.push({ targetName: boneName, mixamoName: normalized });
308
+ }
309
+
310
+ return buildRig('mixamo', entries);
311
+ }
312
+
313
+ // Resolves how a loaded character's bones map onto the canonical roles.
314
+ // Returns { type, targetToMixamo, mixamoToTarget, hipCarrierName } or null
315
+ // when no known convention matches (retargeting is impossible then).
316
+ export function resolveCharacterRig(targetMesh, { vrm = null } = {}) {
317
+ if (vrm) {
318
+ const rig = resolveVrmRig(vrm);
319
+ if (rig) return rig;
320
+ }
321
+
322
+ const bones = targetMesh?.skeleton?.bones || [];
323
+ const boneNames = new Set(bones.map((bone) => bone.name));
324
+ if (boneNames.size === 0) return null;
325
+
326
+ const mmdRig = boneNames.has('下半身') || boneNames.has(MMD_CENTER_BONE)
327
+ ? resolveMmdRig(boneNames)
328
+ : null;
329
+ if (mmdRig) return mmdRig;
330
+
331
+ return resolveMixamoNamedRig(boneNames) || resolveRigifyRig(boneNames);
332
+ }
333
+
334
+ // Preferred target bone name for a canonical role (the skin-weighted variant
335
+ // on rigs that split FK/deform bones), or null when the model lacks it.
336
+ export function targetBoneNameForRole(rig, role) {
337
+ const mixamoName = MIXAMO_BONE_BY_ROLE[role];
338
+ return (mixamoName && rig?.mixamoToTarget.get(mixamoName)) || null;
339
+ }
340
+
341
+ // Every target bone playing a canonical role — MMD leg roles return both the
342
+ // FK bone and its "D" deform twin, so procedural clips can drive all variants
343
+ // the way the retarget bake does.
344
+ export function targetBoneNamesForRole(rig, role) {
345
+ const mixamoName = MIXAMO_BONE_BY_ROLE[role];
346
+ if (!mixamoName || !rig) return [];
347
+
348
+ const names = [];
349
+ for (const [targetName, candidateMixamoName] of rig.targetToMixamo) {
350
+ if (candidateMixamoName === mixamoName) names.push(targetName);
351
+ }
352
+ return names;
353
+ }