@starcried/starlite-vrm 0.1.0-alpha.1 → 0.1.0-alpha.2

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/README.md CHANGED
@@ -29,7 +29,7 @@ const controller = await mountModelWidget(host, {
29
29
  });
30
30
  ```
31
31
 
32
- 上例不需要另备动作文件。运行时自动使用模型名为 `Idle` 的动作,否则采用程序唯一内置的 `StarLiteIdle`(基础站姿与轻呼吸)。可通过 `animations: [{name: "Wave", url: "/Wave.vrma"}]` 添加自己的外部动作,或用 `ambient` 和 `neutralAnimation` 指定基础循环与站姿。不附带模型或其他动作库。显式 `ambient: {animations: []}` 可停用基础循环。眨眼需要有效的标准 Blink 表达,眼球跟随需要正确的 VRM lookAt 映射。模型必须自包含,不接受外部纹理或 buffer 引用。
32
+ 上例不需要另备动作文件。运行时自动使用模型名为 `Idle` 的动作,否则采用程序唯一内置的 `StarLiteIdle`(基础站姿与轻呼吸)。可通过 `animations: [{name: "Wave", url: "/Wave.vrma"}]` 添加自己的外部动作,或用 `ambient` 和 `neutralAnimation` 指定基础循环与站姿。不附带模型或其他动作库。显式 `ambient: {animations: []}` 可停用基础循环。眨眼为可选能力:需要有效的标准 Blink 表达,或完整的左右 Blink 表达;缺少时模型仍可使用,只停用自动眨眼。眼球跟随需要正确的 VRM lookAt 映射。模型必须自包含,不接受外部纹理或 buffer 引用。
33
33
 
34
34
  `inspectVrm(bytes).animations` 返回模型内嵌动作的名称和时长;将名称列表传入 `embeddedAnimations`,即可在 `ambient`、点击与气泡配置中引用。实际加载后 `getAnimations()` 自动列出内嵌动作和外部 VRMA。未命名动作按顺序显示为 `Animation_1` 等;内嵌动作、外部动作名称不得重复,`StarLiteIdle` 为默认 Idle 保留名。角色配置也可放在模型 JSON 的 `extras.starlite.profile`,内容须符合数据型角色配置规范。
35
35
 
@@ -1,4 +1,4 @@
1
- import type { VRM } from "@pixiv/three-vrm";
1
+ import { type VRM } from "@pixiv/three-vrm";
2
2
  import type { CharacterProfile } from "./types.js";
3
3
  export declare function sampleCurve(curve: readonly (readonly [number, number])[], time: number): number;
4
4
  export declare class CharacterBehavior {
package/dist/character.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Quaternion, Vector3 } from "three";
2
+ import { VRMExpressionMorphTargetBind } from "@pixiv/three-vrm";
2
3
  export function sampleCurve(curve, time) {
3
4
  for (let i = 1; i < curve.length; i++) {
4
5
  const a = curve[i - 1], b = curve[i];
@@ -10,6 +11,7 @@ export function sampleCurve(curve, time) {
10
11
  export class CharacterBehavior {
11
12
  #vrm;
12
13
  #profile;
14
+ #blinkExpressions;
13
15
  #morphs = new Map();
14
16
  #earBones = new Map();
15
17
  #baseEar = new Map();
@@ -20,6 +22,12 @@ export class CharacterBehavior {
20
22
  constructor(vrm, profile) {
21
23
  this.#vrm = vrm;
22
24
  this.#profile = profile;
25
+ const usable = (name) => vrm.expressionManager?.getExpression(name)?.binds.some(bind => bind instanceof VRMExpressionMorphTargetBind && bind.weight > 0
26
+ && bind.primitives.some(mesh => mesh.morphTargetInfluences?.[bind.index] !== undefined)) ?? false;
27
+ // Prefer a combined Blink. A complete left/right pair is equivalent; a
28
+ // lone eyelid must not turn an ordinary blink into an unintended wink.
29
+ this.#blinkExpressions = usable("blink") ? ["blink"]
30
+ : usable("blinkLeft") && usable("blinkRight") ? ["blinkLeft", "blinkRight"] : [];
23
31
  if (!profile)
24
32
  return;
25
33
  vrm.scene.traverse((object) => {
@@ -61,9 +69,12 @@ export class CharacterBehavior {
61
69
  // Each custom correction closes its expression's share of the eyelids.
62
70
  // Ordinary blink must still close the remaining Basis share during fades.
63
71
  const curve = this.#profile?.blink.curve ?? [[0, 0], [3.4, 0], [3.48, 1], [3.52, 1], [3.65, 0], [4.7, 0]];
64
- this.#authoredBlink = authoredBlink ? manager?.getValue("blink") ?? 0 : undefined;
65
- this.#blinkAmount = this.#authoredBlink ?? sampleCurve(curve, this.#time % (this.#profile?.blink.period ?? 4.7));
66
- manager?.setValue("blink", Math.max(0, 1 - total) * this.#blinkAmount);
72
+ this.#authoredBlink = authoredBlink ? new Map(this.#blinkExpressions.map(name => [name, manager?.getValue(name) ?? 0])) : undefined;
73
+ this.#blinkAmount = this.#authoredBlink ? Math.max(0, ...this.#authoredBlink.values()) : sampleCurve(curve, this.#time % (this.#profile?.blink.period ?? 4.7));
74
+ for (const name of this.#blinkExpressions) {
75
+ const amount = this.#authoredBlink?.get(name) ?? this.#blinkAmount;
76
+ manager?.setValue(name, Math.max(0, 1 - total) * amount);
77
+ }
67
78
  }
68
79
  afterExpressions() {
69
80
  const profile = this.#profile;
@@ -120,8 +131,8 @@ export class CharacterBehavior {
120
131
  }
121
132
  // Only the applied neutral share changes; the animation's sampled value
122
133
  // remains available to the next authored frame or captured-pose transition.
123
- if (this.#authoredBlink !== undefined)
124
- this.#vrm.expressionManager?.setValue("blink", this.#authoredBlink);
134
+ for (const [name, value] of this.#authoredBlink ?? [])
135
+ this.#vrm.expressionManager?.setValue(name, value);
125
136
  this.#authoredBlink = undefined;
126
137
  }
127
138
  reset() { this.#time = 0; this.#weights = ""; this.#blinkAmount = 0; this.#authoredBlink = undefined; }