@starcried/starlite-vrm 0.1.0-alpha.2 → 0.1.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.
- package/BUNDLED_MOTIONS.json +63 -0
- package/BUNDLED_MOTIONS.md +41 -0
- package/README.en.md +65 -0
- package/README.md +31 -3
- package/THIRD_PARTY_NOTICES.md +2 -0
- package/dist/adjustments.d.ts +23 -0
- package/dist/adjustments.js +201 -0
- package/dist/builtin-motion-catalog.d.ts +16 -0
- package/dist/builtin-motion-catalog.js +10 -0
- package/dist/builtin-motion-data.d.ts +5 -0
- package/dist/builtin-motion-data.js +6 -0
- package/dist/builtin-motions.d.ts +3 -0
- package/dist/builtin-motions.js +10 -0
- package/dist/camera.d.ts +8 -0
- package/dist/camera.js +50 -0
- package/dist/config.cjs +71 -2
- package/dist/config.d.ts +4 -1
- package/dist/config.js +34 -3
- package/dist/environment-lighting.d.ts +12 -0
- package/dist/environment-lighting.js +82 -0
- package/dist/gaze.d.ts +2 -1
- package/dist/gaze.js +17 -6
- package/dist/index.d.ts +4 -1
- package/dist/index.js +240 -197
- package/dist/inspect.cjs +226 -74
- package/dist/inspect.d.ts +56 -9
- package/dist/inspect.js +259 -85
- package/dist/playback.js +25 -3
- package/dist/prepare.cjs +225 -77
- package/dist/prepare.js +4 -3
- package/dist/types.d.ts +22 -1
- package/package.json +6 -3
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"date": "2026-11-03",
|
|
3
|
+
"sourceCandidateSha256": "f9efaedaa9aaaa7f6704001d5760928371fbeb23fd170db33c7b731f258a3cd2",
|
|
4
|
+
"scope": "Humanoid upper-body rotations and optional standard happy/blink only. No meshes, skins, textures, custom model expressions, markers, eye bones, root translations or leg motions.",
|
|
5
|
+
"authorship": {
|
|
6
|
+
"CuriousPeek": "Authored by StarCried for StarLite.",
|
|
7
|
+
"SleepyStartle": "Authored by StarCried for StarLite.",
|
|
8
|
+
"HappyWave": "Adapted from the existing authored Weiguang RaisedHandWave / Desktop_Approval (Warudo AGIA_Other_wave_arm_01); preserves its complete 3.8-second upper-body performance and standard happy expression, with 0.4-second program-Idle entry/exit. Source is not claimed as original."
|
|
9
|
+
},
|
|
10
|
+
"sourceMotionSha256": "f0adbaa2ac42aa7c8c82aa523f2fa5faf82a3e36569efccf0ede9da119b7f9a2",
|
|
11
|
+
"clips": [
|
|
12
|
+
{
|
|
13
|
+
"name": "StarLiteCuriousPeek",
|
|
14
|
+
"duration": 2.0999999046325684,
|
|
15
|
+
"bytes": 17380,
|
|
16
|
+
"sha256": "881238f9c0d6f6d3f0cfabe2d1a02374b60109449f0a0e426242bfd3b2156ee0",
|
|
17
|
+
"bodyTracks": 43,
|
|
18
|
+
"expressions": [],
|
|
19
|
+
"maximumRoundtripError": 3.3306690738754696e-16,
|
|
20
|
+
"officialLoaderValidated": true
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "StarLiteHappyWave",
|
|
24
|
+
"duration": 4.6,
|
|
25
|
+
"bytes": 109148,
|
|
26
|
+
"sha256": "b57e8858bcf44f7b869dde7477e69093dc3a5fdb4c4bb1c0031b894dbb3612fa",
|
|
27
|
+
"bodyTracks": 42,
|
|
28
|
+
"expressions": [
|
|
29
|
+
"happy"
|
|
30
|
+
],
|
|
31
|
+
"maximumRoundtripError": 0,
|
|
32
|
+
"officialLoaderValidated": true,
|
|
33
|
+
"sourceMotion": {
|
|
34
|
+
"file": "blog/motions/RaisedHandWave.vrma",
|
|
35
|
+
"name": "RaisedHandWave / authored Desktop_Approval",
|
|
36
|
+
"sha256": "f0adbaa2ac42aa7c8c82aa523f2fa5faf82a3e36569efccf0ede9da119b7f9a2",
|
|
37
|
+
"duration": 3.8
|
|
38
|
+
},
|
|
39
|
+
"baseline": {
|
|
40
|
+
"name": "StarLiteIdle",
|
|
41
|
+
"sha256": "7bd5ce630f5f624c274ad2de026ad5365297c353d7f265654ed97656a738d248",
|
|
42
|
+
"sampleTime": 0
|
|
43
|
+
},
|
|
44
|
+
"timing": {
|
|
45
|
+
"enterSeconds": 0.4,
|
|
46
|
+
"sourceSeconds": 3.8,
|
|
47
|
+
"exitSeconds": 0.4
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "StarLiteSleepyStartle",
|
|
52
|
+
"duration": 5.5,
|
|
53
|
+
"bytes": 35064,
|
|
54
|
+
"sha256": "d3e9d2583458cc2d63364d44f4b3642bbf7272733c60371933e3a2d2d89910c1",
|
|
55
|
+
"bodyTracks": 43,
|
|
56
|
+
"expressions": [
|
|
57
|
+
"blink"
|
|
58
|
+
],
|
|
59
|
+
"maximumRoundtripError": 4.440892098500626e-16,
|
|
60
|
+
"officialLoaderValidated": true
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Bundled motion provenance
|
|
2
|
+
|
|
3
|
+
StarLite includes four model-independent humanoid motions. No character mesh,
|
|
4
|
+
texture, skin, private Blender project or proprietary expression is included.
|
|
5
|
+
|
|
6
|
+
| Runtime name | Duration | Source |
|
|
7
|
+
| --- | --- | --- |
|
|
8
|
+
| StarLiteIdle | 6 s | See [DEFAULT_IDLE.md](DEFAULT_IDLE.md). |
|
|
9
|
+
| StarLiteCuriousPeek | 2.1 s | Authored by StarCried for StarLite. |
|
|
10
|
+
| StarLiteHappyWave | 4.6 s | The authored Weiguang RaisedHandWave, based on Warudo `AGIA_Other_wave_arm_01`, with its standard happy expression. |
|
|
11
|
+
| StarLiteSleepyStartle | 5.5 s | Authored by StarCried for StarLite; optional standard blink. |
|
|
12
|
+
|
|
13
|
+
The user approved redistribution of selected source motions on 2026-10-10 and
|
|
14
|
+
bundling these three gestures for all imported humanoids on 2026-10-31. This
|
|
15
|
+
records that distribution confirmation, not a claim that Warudo's whole library
|
|
16
|
+
is freely redistributable or that HappyWave is entirely original. Character
|
|
17
|
+
licenses do not apply to or confer rights over the source motion library.
|
|
18
|
+
|
|
19
|
+
`BUNDLED_MOTIONS.json` records source and final clip hashes. The gesture data
|
|
20
|
+
is losslessly deflated inside the runtime, without fetching external files.
|
|
21
|
+
Run `node tools/embed-builtin-motions.mjs <reviewed VRMA directory>` to regenerate
|
|
22
|
+
the embedded data from files matching those hashes. Tests inflate each clip,
|
|
23
|
+
verify hashes, reject artwork/custom expression channels, and retarget through
|
|
24
|
+
the official loader on synthetic VRM 0 and VRM 1 humanoids.
|
|
25
|
+
|
|
26
|
+
Only upper-body/finger rotations are animated; hips, legs, eye bones and root
|
|
27
|
+
translations are not overridden. SleepyStartle uses the standard optional blink
|
|
28
|
+
expression; HappyWave uses optional standard happy. Missing optional fingers
|
|
29
|
+
or expression bindings do not prevent body animation playback.
|
|
30
|
+
HappyWave retains the existing authored 3.8 s RaisedHandWave performance and
|
|
31
|
+
happy curve, including secondary motion on the non-waving side. It does not
|
|
32
|
+
reconstruct the raw 0.6 s library loop. A 0.4 s entry and 0.4 s exit connect its
|
|
33
|
+
raised-arm pose to the program StarLiteIdle reference; no constant clearance
|
|
34
|
+
offset changes the active hand arc. Standard happy uses the target model's
|
|
35
|
+
own expression binding, so its facial appearance varies between models.
|
|
36
|
+
Runtime pose calibration follows the selected Idle without
|
|
37
|
+
rewriting model-authored or external clips. Model-specific clipping can still
|
|
38
|
+
require pose or motion adjustments.
|
|
39
|
+
|
|
40
|
+
Gestures are optional mappings. Adding them never changes an existing user's
|
|
41
|
+
state, click or ambient action selection.
|
package/README.en.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# @starcried/starlite-vrm
|
|
2
|
+
|
|
3
|
+
[简体中文](README.md) · English
|
|
4
|
+
|
|
5
|
+
StarLite's WebGL2 backend for VRM 0/1, embedded glTF animations, external VRMA, limited pointer following, blink, and shared dialogue. The browser entry bundles Three.js, three-vrm, and three-vrm-animation.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @starcried/starlite-vrm
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Mount
|
|
12
|
+
|
|
13
|
+
Provide `<div id="starlite"></div>` and use a bundler:
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { mountModelWidget } from "@starcried/starlite-vrm";
|
|
17
|
+
import "@starcried/starlite-vrm/widget.css";
|
|
18
|
+
|
|
19
|
+
const host = document.querySelector("#starlite");
|
|
20
|
+
if (!host) throw new Error("Missing StarLite host");
|
|
21
|
+
const controller = await mountModelWidget(host, {
|
|
22
|
+
backend: "vrm",
|
|
23
|
+
locale: "en",
|
|
24
|
+
model: "/model.vrm",
|
|
25
|
+
fallback: "/portrait.png",
|
|
26
|
+
dialogue: { click: { lines: ["Hello!", "I'm here."] } },
|
|
27
|
+
interactions: { pointer: true },
|
|
28
|
+
render: { fps: 30, maxDevicePixelRatio: 1.5 },
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
No motion file is required. The runtime uses an authored `Idle` when available, otherwise `StarLiteIdle` supplies a relaxed stance and breathing. Version 0.1.0 also provides optional `StarLiteCuriousPeek`, `StarLiteHappyWave`, and `StarLiteSleepyStartle` gestures for humanoid models. These are reserved names, not character artwork. Assign gestures explicitly; preserve motion provenance in the package notices.
|
|
33
|
+
|
|
34
|
+
Use `animations: [{name: "Wave", url: "/Wave.vrma"}]` for your own VRMA. `ambient` chooses a baseline loop; `neutralAnimation` chooses the stopped pose. An empty ambient list stops baseline playback. Blink needs effective standard Blink bindings or a full left/right pair; absence disables blink only. Eye following needs valid VRM lookAt. Models must be self-contained.
|
|
35
|
+
|
|
36
|
+
`inspectVrm(bytes).animations` lists embedded motion names and durations. Pass names in `embeddedAnimations` to reference them from configuration. `getAnimations()` lists loaded embedded, external, and built-in motions. Names must be unique and case-sensitive. Data-only character configuration can be embedded as `extras.starlite.profile`.
|
|
37
|
+
|
|
38
|
+
Call `destroy()` when unmounting. Hidden models stop animation and physics; resume does not catch up hidden time. A bubble scene may omit animation entirely. `linesByAnimation` must match its selected pool.
|
|
39
|
+
|
|
40
|
+
## Live adjustments and diagnostics
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
controller.setModelAdjustments({
|
|
44
|
+
idle: { armLiftDegrees: 10, palmTwistDegrees: 5, wristLiftDegrees: 8, handCurl: 0.2, elbowBendDegrees: 8 },
|
|
45
|
+
gaze: { headStrength: 0.8, eyeStrength: 1, smoothingMs: 120 },
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This replaces current adjustments; `{}` restores originals without reloading or restarting motion. Pose parameters calibrate `StarLiteIdle`; the three program gestures begin and end in the selected Idle reference pose, including calibrated arms, wrists and hands. An authored Idle can also supply that reference without itself being modified. Body/upper-arm relative motion is retained. Animated forearms and fingers align smoothly only at entry/exit and keep their authored active shapes, avoiding excessive elbow bending or doubled finger curl. Model-authored and external motions remain unchanged. Upper-arm lift: −30–45°; palm twist and wrist lift: −45–45°; elbow correction: −20–45°; hand curl: 0–1, omitted to keep authored fingers. Head/eye strength is 0–2; smoothing is 20–1000 ms. Missing mappings cannot be created by sliders.
|
|
50
|
+
|
|
51
|
+
`performance: {show: true}` or `setPerformanceVisible(true)` enables local FPS and timing diagnostics. CPU-side frame cost is not measured GPU execution or CPU utilization. Node's `@starcried/starlite-vrm/inspect` returns capabilities and logical metrics; errors expose controlled codes without private paths or external URIs.
|
|
52
|
+
|
|
53
|
+
[Configuration](https://github.com/StarCried/starlite/blob/main/docs/configuration.en.md) · [Hexo adapter](https://github.com/StarCried/starlite/blob/main/packages/hexo/README.en.md) · [Shared API](https://github.com/StarCried/starlite/blob/main/packages/core/README.en.md)
|
|
54
|
+
|
|
55
|
+
Allow appropriate `img-src blob:` and `connect-src blob:` for embedded textures in CSP. To avoid downloading JavaScript on mobile, gate a dynamic import before loading runtime; the static import above does not do that.
|
|
56
|
+
|
|
57
|
+
Program code: BSD-2-Clause. Models and motion sources retain their own licenses.
|
|
58
|
+
|
|
59
|
+
### Environment fill light
|
|
60
|
+
|
|
61
|
+
Optional `environmentLighting: { color: "#b7cfff", strength: 1 }` adds environmental colour using the existing lights. Use six-digit RGB and strength 0–2 (default 1, or 100%). The ordinary range is 0–100%; 100–200% exaggerates colour and brightness response. Part of the side-key energy moves into the ambient fill; the key remains white, model materials/textures stay unchanged, and no lights are added.
|
|
62
|
+
|
|
63
|
+
At 100% strength, inferred brightness targets are 80% for black, approximately 100% for middle grey and 110% for white. Optional `brightness: 0.95` explicitly sets a target in 0.8–1.1. Strength controls colour and brightness together: at 20%, the effective light multiplier is approximately 90.24%–104.88%; at 200%, it expands to 60%–120%. These bounds describe light energy, not each material's screen-pixel brightness. Chroma is luminance-normalized to avoid double darkening, and near-black colour noise is suppressed.
|
|
64
|
+
|
|
65
|
+
`controller.setEnvironmentLighting(...)` smoothly updates colours and both light intensities together. `null` or strength 0 restores the original lighting. While paused, a change redraws the still pose without advancing animation. Omitting the option keeps the original lighting; the runtime never captures the screen, and the host supplies the colour.
|
package/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# @starcried/starlite-vrm
|
|
2
2
|
|
|
3
|
+
简体中文 · [English](README.en.md)
|
|
4
|
+
|
|
3
5
|
StarLite 的 WebGL2 VRM 0/1 后端,支持模型内嵌 glTF 动作、外部 VRMA、限角光标跟随、眨眼和公共气泡。浏览器入口已打包 Three.js、three-vrm 和 three-vrm-animation。
|
|
4
6
|
|
|
5
7
|
在 Node.js 22.12+ 项目中安装 alpha 版本:
|
|
6
8
|
|
|
7
9
|
```sh
|
|
8
|
-
npm install @starcried/starlite-vrm
|
|
10
|
+
npm install @starcried/starlite-vrm
|
|
9
11
|
```
|
|
10
12
|
|
|
11
13
|
## 挂载
|
|
@@ -29,9 +31,9 @@ const controller = await mountModelWidget(host, {
|
|
|
29
31
|
});
|
|
30
32
|
```
|
|
31
33
|
|
|
32
|
-
上例不需要另备动作文件。运行时自动使用模型名为 `Idle`
|
|
34
|
+
上例不需要另备动作文件。运行时自动使用模型名为 `Idle` 的动作,否则采用程序内置的 `StarLiteIdle`(基础站姿与轻呼吸)。可通过 `animations: [{name: "Wave", url: "/Wave.vrma"}]` 添加自己的外部动作,或用 `ambient` 和 `neutralAnimation` 指定基础循环与站姿。当前源码还提供 `StarLiteCuriousPeek`、`StarLiteHappyWave`、`StarLiteSleepyStartle` 三个通用人形动作,供所有模型按需映射;不附带模型素材,动作来源保留独立说明。显式 `ambient: {animations: []}` 可停用基础循环。眨眼为可选能力:需要有效的标准 Blink 表达,或完整的左右 Blink 表达;缺少时模型仍可使用,只停用自动眨眼。眼球跟随需要正确的 VRM lookAt 映射。模型必须自包含,不接受外部纹理或 buffer 引用。
|
|
33
35
|
|
|
34
|
-
`inspectVrm(bytes).animations` 返回模型内嵌动作的名称和时长;将名称列表传入 `embeddedAnimations`,即可在 `ambient`、点击与气泡配置中引用。实际加载后 `getAnimations()` 自动列出内嵌动作和外部 VRMA。未命名动作按顺序显示为 `Animation_1`
|
|
36
|
+
`inspectVrm(bytes).animations` 返回模型内嵌动作的名称和时长;将名称列表传入 `embeddedAnimations`,即可在 `ambient`、点击与气泡配置中引用。实际加载后 `getAnimations()` 自动列出内嵌动作和外部 VRMA。未命名动作按顺序显示为 `Animation_1` 等;内嵌动作、外部动作名称不得重复,上述四个 `StarLite` 动作名均为保留名。角色配置也可放在模型 JSON 的 `extras.starlite.profile`,内容须符合数据型角色配置规范。
|
|
35
37
|
|
|
36
38
|
`pause()`/`resume()` 控制更新,`getAnimations()` 列出动作,`playAnimation()`/`stopAnimation()` 控制播放;页面组件卸载时调用 `controller.destroy()` 释放资源。`neutralAnimation` 的首帧作为停止后的基础站姿,避免无角色配置的模型回到 T 姿势。动作名区分大小写;循环在停止或被打断后结束等待。隐藏时停止动画和物理,恢复时不补算离线时间。
|
|
37
39
|
|
|
@@ -39,8 +41,34 @@ const controller = await mountModelWidget(host, {
|
|
|
39
41
|
|
|
40
42
|
## 进阶
|
|
41
43
|
|
|
44
|
+
`adjustments` 为当前模型提供可选的姿态与跟随校准,例如:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
controller.setModelAdjustments({
|
|
48
|
+
idle: { armLiftDegrees: 10, palmTwistDegrees: 5, wristLiftDegrees: 8, handCurl: 0.2, elbowBendDegrees: 8 },
|
|
49
|
+
gaze: { headStrength: 0.8, eyeStrength: 1, smoothingMs: 120 },
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
相同对象也可作为挂载配置的 `adjustments`。该方法整体替换当前调节,`{}` 恢复原样;实时调节不重新加载模型、不重启动作。双臂抬高范围为 −30~45°,掌心扭转和手腕抬高均为 −45~45°,肘部弯曲为 −20~45°;掌心调节左右对称;手腕抬高在前臂弯曲平面调整手部方向,正值向上折,不移动腕部位置,不改变上臂、前臂或掌心扭转的方向约定。手部张合为 0~1,省略时保留原动作,0 张开、1 握拳,仅处理已映射的标准手指。姿态参数调整程序内置 `StarLiteIdle`;三个程序通用动作以当前选定 Idle 的基础站姿起止,因此会沿用调好的手臂、手腕和手部姿态。也可沿用模型自带 Idle 的站姿,但不会修改该 Idle。通用动作保留身体与上臂的相对运动;有动作的前臂和手指仅在进入、退出时平滑对齐基础姿态,挥手过程中保留原动作的肘部与手型,避免叠加校准后过度弯曲。模型自带动作和外部 VRMA 保持原样。头眼跟随强度各为 0~2,默认 1;0 停用对应跟随,仍尊重现有限角和前景动作方向。平滑时间为 20~1000 ms,省略时沿用 `gaze.smoothingMs`。模型缺少眼球绑定时,调节不能生成眼球跟随。
|
|
54
|
+
|
|
55
|
+
`0.1.0-alpha.3` 提供 `performance: {show: true}`(默认关闭),也可用
|
|
56
|
+
`controller.setPerformanceVisible?.(true)` 即时切换。含义和测量边界见
|
|
57
|
+
[性能预算](https://github.com/StarCried/starlite/blob/main/docs/performance.md)。
|
|
58
|
+
Node 侧 `@starcried/starlite-vrm/inspect` 返回 `capabilities` 与逻辑 `metrics`;
|
|
59
|
+
`VrmInspectionError.code/details` 提供受控原因及缺失骨骼名,不回显模型路径或外部 URI。
|
|
60
|
+
见[兼容说明](https://github.com/StarCried/starlite/blob/main/docs/model-compatibility.md)。
|
|
61
|
+
|
|
42
62
|
[动作、句库、相机与性能配置](https://github.com/StarCried/starlite/blob/main/docs/configuration.md) · [构建博客资源](https://github.com/StarCried/starlite/blob/main/docs/examples/prepare-vrm.cjs) · [Hexo 插件](https://github.com/StarCried/starlite/blob/main/packages/hexo/README.md) · [开发与验证](https://github.com/StarCried/starlite/blob/main/docs/development.md)
|
|
43
63
|
|
|
44
64
|
配置 CSP 的网站须为模型内嵌贴图允许适用的 `img-src blob:` 和 `connect-src blob:`。独立网页若要在手机上避免加载运行时,须在检查屏宽后动态导入;上例的静态导入会下载 JS。
|
|
45
65
|
|
|
46
66
|
BSD-2-Clause;模型和动作由使用者按各自许可提供,微光适配为可选配置。
|
|
67
|
+
|
|
68
|
+
### 环境补光
|
|
69
|
+
|
|
70
|
+
可选 `environmentLighting: { color: "#b7cfff", strength: 1 }` 给现有灯光加入环境色;颜色使用六位 RGB,强度范围为 0–2,默认 1(100%)。0–100% 为普通调节范围,100–200% 进一步增强染色和明暗反应。程序会将一部分侧向主光能量分配到环境补光;主光仍为白色,不修改模型材质和贴图,也不增加灯光数量。
|
|
71
|
+
|
|
72
|
+
默认根据颜色亮度调整总光照:在 100% 强度下,黑色目标为 80%、中灰约 100%、白色 110%。可用 `brightness: 0.95` 显式指定 0.8–1.1 内的目标。强度同时控制颜色与亮度:降低到 20% 时有效光照倍率约为 90.24%–104.88%;提高到 200% 时明暗范围扩大到 60%–120%。这是灯光倍率,不保证所有材质的屏幕像素亮度按同一比例变化。色相经过亮度归一化,避免暗色重复降低亮度;近黑色取色噪声会被抑制。
|
|
73
|
+
|
|
74
|
+
运行中调用 `controller.setEnvironmentLighting(...)` 同步平滑改变颜色与光强,传 `null` 或强度设为 0 恢复原灯光。暂停时只重绘静止画面,不推进动作。不配置此选项时保持原灯光;运行时不读取屏幕,颜色来源由宿主提供。
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -10,3 +10,5 @@ layer originating from Warudo's Human@SM Breathe. Its hash, authorship and the
|
|
|
10
10
|
user's redistribution confirmation are recorded in DEFAULT_IDLE.md; it is
|
|
11
11
|
not represented as a wholly original or separately MIT-licensed motion.
|
|
12
12
|
Other model and motion rights remain the responsibility of their owner.
|
|
13
|
+
|
|
14
|
+
Bundled humanoid gestures have separate recorded provenance; see [BUNDLED_MOTIONS.md](BUNDLED_MOTIONS.md) and `BUNDLED_MOTIONS.json`. They include no model artwork.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AnimationClip, type Object3D } from "three";
|
|
2
|
+
import type { VRMHumanoid, VRMPose } from "@pixiv/three-vrm";
|
|
3
|
+
import type { ModelAdjustments } from "./types.js";
|
|
4
|
+
type Idle = NonNullable<ModelAdjustments["idle"]>;
|
|
5
|
+
/** A controller-owned derivative of the program Idle; authored clips never enter here. */
|
|
6
|
+
export declare class AdjustableIdle {
|
|
7
|
+
#private;
|
|
8
|
+
readonly clip: AnimationClip;
|
|
9
|
+
constructor(source: AnimationClip, root: Object3D, humanoid: Pick<VRMHumanoid, "getNormalizedBoneNode">, metaVersion?: "0" | "1");
|
|
10
|
+
update(idle?: Idle): void;
|
|
11
|
+
/** Refresh stopped/paused neutral without sampling or resetting the live mixer. */
|
|
12
|
+
neutralPose(pose: VRMPose): VRMPose;
|
|
13
|
+
}
|
|
14
|
+
/** Controller-owned program gestures expressed around the selected neutral pose.
|
|
15
|
+
* Only generated built-ins enter here; source clips and authored motions stay intact.
|
|
16
|
+
*/
|
|
17
|
+
export declare class RebasedBuiltinGesture {
|
|
18
|
+
#private;
|
|
19
|
+
readonly clip: AnimationClip;
|
|
20
|
+
constructor(source: AnimationClip, root: Object3D, humanoid: Pick<VRMHumanoid, "getNormalizedBoneNode">, pose: VRMPose);
|
|
21
|
+
update(pose: VRMPose): void;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { AnimationClip, MathUtils, PropertyBinding, Quaternion, Vector3 } from "three";
|
|
2
|
+
/** A controller-owned derivative of the program Idle; authored clips never enter here. */
|
|
3
|
+
export class AdjustableIdle {
|
|
4
|
+
clip;
|
|
5
|
+
#targets = [];
|
|
6
|
+
#basis;
|
|
7
|
+
constructor(source, root, humanoid, metaVersion = "1") {
|
|
8
|
+
// Match createVRMAnimationClip: VRM0 faces -Z, so its normalized rig's
|
|
9
|
+
// X/Z rotation axes are the opposite of VRM1's character-space axes.
|
|
10
|
+
this.#basis = metaVersion === "0" ? -1 : 1;
|
|
11
|
+
const bones = new Map();
|
|
12
|
+
for (const [side, sign] of [["left", 1], ["right", -1]]) {
|
|
13
|
+
const add = (suffix, kind, joint) => {
|
|
14
|
+
const bone = (side + suffix), node = humanoid.getNormalizedBoneNode(bone);
|
|
15
|
+
if (node)
|
|
16
|
+
bones.set(node, { bone, side: sign, kind, ...(joint ? { joint } : {}) });
|
|
17
|
+
};
|
|
18
|
+
add("UpperArm", "arm");
|
|
19
|
+
add("LowerArm", "elbow");
|
|
20
|
+
add("Hand", "palm");
|
|
21
|
+
for (const finger of ["Index", "Middle", "Ring", "Little"]) {
|
|
22
|
+
for (const joint of ["Proximal", "Intermediate", "Distal"])
|
|
23
|
+
add(finger + joint, "finger", joint);
|
|
24
|
+
}
|
|
25
|
+
for (const joint of ["Metacarpal", "Proximal", "Distal"])
|
|
26
|
+
add("Thumb" + joint, "thumb", joint);
|
|
27
|
+
}
|
|
28
|
+
const tracks = source.tracks.map(track => {
|
|
29
|
+
const parsed = PropertyBinding.parseTrackName(track.name);
|
|
30
|
+
const node = PropertyBinding.findNode(root, parsed.nodeName);
|
|
31
|
+
const bone = node ? bones.get(node) : undefined;
|
|
32
|
+
if (!bone || parsed.propertyName !== "quaternion" || track.getValueSize() !== 4 || track.ValueTypeName !== "quaternion")
|
|
33
|
+
return track;
|
|
34
|
+
const copy = track.clone();
|
|
35
|
+
this.#targets.push({ ...bone, source: track, track: copy });
|
|
36
|
+
return copy;
|
|
37
|
+
});
|
|
38
|
+
this.clip = new AnimationClip(source.name, source.duration, tracks, source.blendMode);
|
|
39
|
+
}
|
|
40
|
+
update(idle = {}) {
|
|
41
|
+
const rotation = new Quaternion(), correction = new Quaternion(), wrist = new Quaternion(), motion = new Quaternion(), initial = new Quaternion(), axis = new Vector3();
|
|
42
|
+
for (const target of this.#targets) {
|
|
43
|
+
const { source, track, side, kind } = target;
|
|
44
|
+
track.values.set(source.values);
|
|
45
|
+
const degrees = kind === "arm" ? idle.armLiftDegrees ?? 0
|
|
46
|
+
: kind === "palm" ? idle.palmTwistDegrees ?? 0 : kind === "elbow" ? idle.elbowBendDegrees ?? 0 : 0;
|
|
47
|
+
const hand = kind === "finger" || kind === "thumb";
|
|
48
|
+
const wristDegrees = kind === "palm" ? idle.wristLiftDegrees ?? 0 : 0;
|
|
49
|
+
if ((!hand && !degrees && !wristDegrees) || (hand && idle.handCurl === undefined))
|
|
50
|
+
continue;
|
|
51
|
+
if (hand) {
|
|
52
|
+
initial.fromArray(source.values, 0).invert();
|
|
53
|
+
// The four fingers curl towards the palm. The thumb has its own
|
|
54
|
+
// opposition and flexion recipe rather than sharing their bend axis.
|
|
55
|
+
const curl = idle.handCurl;
|
|
56
|
+
if (kind === "finger") {
|
|
57
|
+
const bend = target.joint === "Proximal" ? 70 : target.joint === "Intermediate" ? 90 : 60;
|
|
58
|
+
correction.setFromAxisAngle(axis.set(0, 0, -side * this.#basis), MathUtils.degToRad(bend * curl));
|
|
59
|
+
}
|
|
60
|
+
else if (target.joint === "Metacarpal") {
|
|
61
|
+
correction.setFromAxisAngle(axis.set(0, side, 0), MathUtils.degToRad(35 * curl));
|
|
62
|
+
correction.multiply(rotation.setFromAxisAngle(axis.set(0, 0, -side * this.#basis), MathUtils.degToRad(15 * curl)));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
correction.setFromAxisAngle(axis.set(0, 0, -side * this.#basis), MathUtils.degToRad((target.joint === "Proximal" ? 45 : 65) * curl));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// A rotation axis is an axial vector: mirroring left/right across X
|
|
70
|
+
// keeps its X sign and reverses Y/Z. Opposite palm-X signs would
|
|
71
|
+
// turn one palm inward and the other outward.
|
|
72
|
+
axis.set(kind === "palm" ? this.#basis : 0, kind === "elbow" ? -side : 0, kind === "arm" ? side * this.#basis : 0);
|
|
73
|
+
correction.setFromAxisAngle(axis, MathUtils.degToRad(degrees));
|
|
74
|
+
if (kind === "palm")
|
|
75
|
+
wrist.setFromAxisAngle(axis.set(0, 0, side * this.#basis), MathUtils.degToRad(wristDegrees));
|
|
76
|
+
}
|
|
77
|
+
for (let at = 0; at < source.values.length; at += 4) {
|
|
78
|
+
rotation.fromArray(source.values, at);
|
|
79
|
+
if (hand) {
|
|
80
|
+
// Keep the source motion around its first sample, including tiny
|
|
81
|
+
// breathing changes, while replacing only the static finger pose.
|
|
82
|
+
motion.copy(initial).multiply(rotation);
|
|
83
|
+
rotation.copy(correction).multiply(motion);
|
|
84
|
+
}
|
|
85
|
+
else if (kind === "palm") {
|
|
86
|
+
// Bend in the forearm's frame before applying the palm-local twist,
|
|
87
|
+
// so changing palm direction does not rotate the wrist-lift axis.
|
|
88
|
+
rotation.premultiply(wrist).multiply(correction);
|
|
89
|
+
}
|
|
90
|
+
else if (kind === "arm")
|
|
91
|
+
rotation.premultiply(correction);
|
|
92
|
+
else
|
|
93
|
+
rotation.multiply(correction);
|
|
94
|
+
rotation.normalize().toArray(track.values, at);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Refresh stopped/paused neutral without sampling or resetting the live mixer. */
|
|
99
|
+
neutralPose(pose) {
|
|
100
|
+
const result = { ...pose };
|
|
101
|
+
for (const { bone, track } of this.#targets) {
|
|
102
|
+
result[bone] = { ...result[bone], rotation: Array.from(track.values.slice(0, 4)) };
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Controller-owned program gestures expressed around the selected neutral pose.
|
|
108
|
+
* Only generated built-ins enter here; source clips and authored motions stay intact.
|
|
109
|
+
*/
|
|
110
|
+
export class RebasedBuiltinGesture {
|
|
111
|
+
clip;
|
|
112
|
+
#targets = [];
|
|
113
|
+
constructor(source, root, humanoid, pose) {
|
|
114
|
+
const bones = new Map();
|
|
115
|
+
for (const [name, value] of Object.entries(pose)) {
|
|
116
|
+
if (!value?.rotation)
|
|
117
|
+
continue;
|
|
118
|
+
const bone = name, node = humanoid.getNormalizedBoneNode(bone);
|
|
119
|
+
if (node)
|
|
120
|
+
bones.set(node, bone);
|
|
121
|
+
}
|
|
122
|
+
const tracks = source.tracks.map(track => {
|
|
123
|
+
const parsed = PropertyBinding.parseTrackName(track.name);
|
|
124
|
+
const node = PropertyBinding.findNode(root, parsed.nodeName);
|
|
125
|
+
const bone = node ? bones.get(node) : undefined;
|
|
126
|
+
if (!bone || parsed.propertyName !== "quaternion" || track.getValueSize() !== 4 || track.ValueTypeName !== "quaternion")
|
|
127
|
+
return track;
|
|
128
|
+
const first = new Quaternion().fromArray(track.values, 0).normalize(), sample = new Quaternion();
|
|
129
|
+
let boundaryOnly = false;
|
|
130
|
+
if (/LowerArm|Thumb|Index|Middle|Ring|Little/.test(bone))
|
|
131
|
+
for (let at = 4; at < track.values.length; at += 4) {
|
|
132
|
+
if (first.angleTo(sample.fromArray(track.values, at).normalize()) > 1e-4) {
|
|
133
|
+
boundaryOnly = true;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const original = track;
|
|
138
|
+
const base = boundaryOnly ? sampleGestureBoundaries(original, source.duration) : original;
|
|
139
|
+
const copy = base.clone();
|
|
140
|
+
this.#targets.push({ bone, source: base, track: copy, boundaryOnly });
|
|
141
|
+
return copy;
|
|
142
|
+
});
|
|
143
|
+
this.clip = new AnimationClip(source.name, source.duration, tracks, source.blendMode);
|
|
144
|
+
this.update(pose);
|
|
145
|
+
}
|
|
146
|
+
update(pose) {
|
|
147
|
+
const correction = new Quaternion(), first = new Quaternion(), sample = new Quaternion(), boundaryCorrection = new Quaternion();
|
|
148
|
+
const ramp = Math.min(.4, this.clip.duration / 2);
|
|
149
|
+
for (const { bone, source, track, boundaryOnly } of this.#targets) {
|
|
150
|
+
track.values.set(source.values);
|
|
151
|
+
const target = pose[bone]?.rotation;
|
|
152
|
+
if (!target)
|
|
153
|
+
continue;
|
|
154
|
+
correction.fromArray(target).normalize().multiply(first.fromArray(source.values, 0).normalize().invert());
|
|
155
|
+
for (let at = 0; at < source.values.length; at += 4) {
|
|
156
|
+
// q(t) = targetIdle * inverse(originalFirst) * originalSample(t).
|
|
157
|
+
// Rebuild from immutable source each time, retaining local motion and
|
|
158
|
+
// mixer-owned sample arrays rather than accumulating calibration.
|
|
159
|
+
let offset = correction;
|
|
160
|
+
if (boundaryOnly && ramp > 0) {
|
|
161
|
+
// Full offsets can overfold an authored elbow or double a fist's
|
|
162
|
+
// curl. Match rest at the boundaries, retaining active elbow/finger
|
|
163
|
+
// shapes. Static tracks continue to follow the adjusted rest.
|
|
164
|
+
const time = source.times[at / 4];
|
|
165
|
+
const t = MathUtils.clamp(Math.min(time, this.clip.duration - time) / ramp, 0, 1);
|
|
166
|
+
const weight = 1 - t * t * (3 - 2 * t);
|
|
167
|
+
if (weight === 0)
|
|
168
|
+
continue;
|
|
169
|
+
offset = boundaryCorrection.identity().slerp(correction, weight);
|
|
170
|
+
}
|
|
171
|
+
sample.fromArray(source.values, at).premultiply(offset).normalize().toArray(track.values, at);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/** Sparse authored first/last keys would otherwise linearize the eased offset.
|
|
177
|
+
* Add at most 50 samples per affected track, only in the bounded two ramps.
|
|
178
|
+
*/
|
|
179
|
+
function sampleGestureBoundaries(source, duration) {
|
|
180
|
+
const ramp = Math.min(.4, duration / 2);
|
|
181
|
+
if (ramp <= 0)
|
|
182
|
+
return source;
|
|
183
|
+
const times = new Set(source.times);
|
|
184
|
+
const steps = Math.ceil(ramp * 60);
|
|
185
|
+
for (let step = 0; step <= steps; step++) {
|
|
186
|
+
const time = Math.min(ramp, step / 60);
|
|
187
|
+
times.add(Math.fround(time));
|
|
188
|
+
times.add(Math.fround(duration - time));
|
|
189
|
+
}
|
|
190
|
+
const base = source.clone();
|
|
191
|
+
base.times = new Float32Array([...times].sort((a, b) => a - b));
|
|
192
|
+
base.values = new Float32Array(base.times.length * 4);
|
|
193
|
+
const interpolant = source.createInterpolant(new Float32Array(4));
|
|
194
|
+
// Preserve every original timestamp/value exactly, including interior keys.
|
|
195
|
+
const originals = new Map(Array.from(source.times, (time, index) => [time, index]));
|
|
196
|
+
for (let index = 0; index < base.times.length; index++) {
|
|
197
|
+
const time = base.times[index], original = originals.get(time);
|
|
198
|
+
base.values.set(original === undefined ? interpolant.evaluate(time) : source.values.subarray(original * 4, original * 4 + 4), index * 4);
|
|
199
|
+
}
|
|
200
|
+
return base;
|
|
201
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Program motions are independent of model artwork and need no network request. */
|
|
2
|
+
export declare const BUILTIN_ANIMATIONS: readonly [{
|
|
3
|
+
readonly name: "StarLiteIdle";
|
|
4
|
+
readonly durationMs: 6000;
|
|
5
|
+
}, {
|
|
6
|
+
readonly name: "StarLiteCuriousPeek";
|
|
7
|
+
readonly durationMs: 2100;
|
|
8
|
+
}, {
|
|
9
|
+
readonly name: "StarLiteHappyWave";
|
|
10
|
+
readonly durationMs: 4600;
|
|
11
|
+
}, {
|
|
12
|
+
readonly name: "StarLiteSleepyStartle";
|
|
13
|
+
readonly durationMs: 5500;
|
|
14
|
+
}];
|
|
15
|
+
export type BuiltinAnimationName = (typeof BUILTIN_ANIMATIONS)[number]["name"];
|
|
16
|
+
export declare function isBuiltinAnimation(name: string): name is BuiltinAnimationName;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Program motions are independent of model artwork and need no network request. */
|
|
2
|
+
export const BUILTIN_ANIMATIONS = [
|
|
3
|
+
{ name: "StarLiteIdle", durationMs: 6000 },
|
|
4
|
+
{ name: "StarLiteCuriousPeek", durationMs: 2100 },
|
|
5
|
+
{ name: "StarLiteHappyWave", durationMs: 4600 },
|
|
6
|
+
{ name: "StarLiteSleepyStartle", durationMs: 5500 },
|
|
7
|
+
];
|
|
8
|
+
export function isBuiltinAnimation(name) {
|
|
9
|
+
return BUILTIN_ANIMATIONS.some(animation => animation.name === name);
|
|
10
|
+
}
|