@pmndrs/viverse 0.1.19 → 0.1.20

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.
@@ -0,0 +1,24 @@
1
+ {
2
+ "Hips": "hips",
3
+ "Spine": "spine",
4
+ "Spine1": "chest",
5
+ "Spine2": "upperChest",
6
+ "Neck": "neck",
7
+ "Head": "head",
8
+ "LeftShoulder": "leftShoulder",
9
+ "LeftArm": "leftUpperArm",
10
+ "LeftForeArm": "leftLowerArm",
11
+ "LeftHand": "leftHand",
12
+ "RightShoulder": "rightShoulder",
13
+ "RightArm": "rightUpperArm",
14
+ "RightForeArm": "rightLowerArm",
15
+ "RightHand": "rightHand",
16
+ "LeftUpLeg": "leftUpperLeg",
17
+ "LeftLeg": "leftLowerLeg",
18
+ "LeftFoot": "leftFoot",
19
+ "LeftToe": "leftToes",
20
+ "RightUpLeg": "rightUpperLeg",
21
+ "RightLeg": "rightLowerLeg",
22
+ "RightFoot": "rightFoot",
23
+ "RightToe": "rightToes"
24
+ }
@@ -0,0 +1,4 @@
1
+ import { AnimationClip } from 'three';
2
+ import { loadCharacterModel } from '../model/index.js';
3
+ import type { VRMHumanBoneName } from '@pixiv/three-vrm';
4
+ export declare function loadVrmModelBvhAnimations(model: Exclude<Awaited<ReturnType<typeof loadCharacterModel>>, undefined>, url: string, removeXZMovement: boolean, boneMap?: Record<string, VRMHumanBoneName>): Promise<Array<AnimationClip>>;
@@ -0,0 +1,8 @@
1
+ import { BVHLoader } from 'three/examples/jsm/Addons.js';
2
+ import { bvhBoneMap, fixModelAnimationClip } from './index.js';
3
+ const loader = new BVHLoader();
4
+ export async function loadVrmModelBvhAnimations(model, url, removeXZMovement, boneMap) {
5
+ const clipScene = await loader.loadAsync(url);
6
+ fixModelAnimationClip(model, clipScene.clip, undefined, removeXZMovement, boneMap ?? bvhBoneMap);
7
+ return [clipScene.clip];
8
+ }
@@ -1,13 +1,13 @@
1
1
  import { VRMHumanBoneName } from '@pixiv/three-vrm';
2
2
  import { AnimationClip, Object3D } from 'three';
3
3
  import { loadCharacterModel } from '../model/index.js';
4
- export declare function fixModelAnimationClip(model: Exclude<Awaited<ReturnType<typeof loadCharacterModel>>, undefined>, clip: AnimationClip, clipScene: Object3D, removeXZMovement: boolean, boneMap?: Record<string, VRMHumanBoneName>): void;
4
+ export declare function fixModelAnimationClip(model: Exclude<Awaited<ReturnType<typeof loadCharacterModel>>, undefined>, clip: AnimationClip, clipScene: Object3D | undefined, removeXZMovement: boolean, boneMap?: Record<string, VRMHumanBoneName>): void;
5
5
  export * from './gltf.js';
6
6
  export * from './fbx.js';
7
7
  export * from './vrma.js';
8
8
  export * from './utils.js';
9
9
  export type ModelAnimationOptions = {
10
- type: 'mixamo' | 'gltf' | 'vrma' | 'fbx';
10
+ type?: 'mixamo' | 'gltf' | 'vrma' | 'fbx' | 'bvh';
11
11
  url: string;
12
12
  removeXZMovement?: boolean;
13
13
  trimTime?: {
@@ -30,3 +30,4 @@ declare const simpleCharacterAnimationUrls: {
30
30
  export declare const simpleCharacterAnimationNames: Array<keyof typeof simpleCharacterAnimationUrls>;
31
31
  export declare function getSimpleCharacterModelAnimationOptions(animationName: keyof typeof simpleCharacterAnimationUrls): Promise<ModelAnimationOptions>;
32
32
  export declare const mixamoBoneMap: Record<string, VRMHumanBoneName>;
33
+ export declare const bvhBoneMap: Record<string, VRMHumanBoneName>;
@@ -1,11 +1,13 @@
1
1
  import { VRM } from '@pixiv/three-vrm';
2
2
  import { Euler, Quaternion, QuaternionKeyframeTrack, Vector3, VectorKeyframeTrack, } from 'three';
3
+ import _bvhBoneMap from './bvh-bone-map.json';
4
+ import { loadVrmModelBvhAnimations } from './bvh.js';
3
5
  import { loadVrmModelFbxAnimations } from './fbx.js';
4
6
  import { loadVrmModelGltfAnimations } from './gltf.js';
7
+ import _mixamoBoneMap from './mixamo-bone-map.json';
5
8
  import { scaleAnimationClipTime, trimAnimationClip } from './utils.js';
6
9
  import { loadVrmModelVrmaAnimations } from './vrma.js';
7
10
  import { cached } from '../utils.js';
8
- import _mixamoBoneMap from './mixamo-bone-map.json';
9
11
  //helper variables for the quaternion retargeting
10
12
  const baseThisLocalRestRotation_inverse = new Quaternion();
11
13
  const baseThisLocalCurrentRotation = new Quaternion();
@@ -20,41 +22,77 @@ const position = new Vector3();
20
22
  const nonVrmRotationOffset = new Quaternion().setFromEuler(new Euler(0, Math.PI, 0));
21
23
  //TODO: currently assumes the model is not yet transformed - loaded for the first time
22
24
  export function fixModelAnimationClip(model, clip, clipScene, removeXZMovement, boneMap) {
23
- const hipsBoneName = boneMap == null ? 'hips' : Object.entries(boneMap).find(([, vrmBoneName]) => vrmBoneName === 'hips')?.[0];
24
- if (hipsBoneName == null) {
25
+ const hipsClipBoneName = boneMap == null ? 'hips' : Object.entries(boneMap).find(([, vrmBoneName]) => vrmBoneName === 'hips')?.[0];
26
+ if (hipsClipBoneName == null) {
25
27
  throw new Error('Failed to determine hips bone name for VRM animation. Please check the bone map or animation file.');
26
28
  }
27
- const clipSceneHips = clipScene.getObjectByName(hipsBoneName);
28
- clipSceneHips?.parent?.updateMatrixWorld();
29
- const vrmHipsPosition = model instanceof VRM
30
- ? model.humanoid.normalizedRestPose.hips?.position
31
- : model.scene.getObjectByName('hips')?.position.toArray();
32
- if (clipSceneHips == null || vrmHipsPosition == null) {
33
- throw new Error('Failed to load VRM animation: missing animation hips object or VRM hips position.');
29
+ let restRoot;
30
+ let restRootParent;
31
+ if (!(model instanceof VRM)) {
32
+ restRoot = model.scene.getObjectByName('rest_root');
33
+ if (restRoot == null) {
34
+ throw new Error(`Model rest root not found.`);
35
+ }
36
+ restRootParent = restRoot?.parent;
37
+ restRoot.parent = null;
38
+ }
39
+ let positionScale = 1;
40
+ let clipSceneHips;
41
+ if (clipScene != null) {
42
+ clipSceneHips = clipScene.getObjectByName(hipsClipBoneName);
43
+ clipSceneHips?.parent?.updateMatrixWorld();
44
+ const vrmHipsPosition = model instanceof VRM
45
+ ? model.humanoid.normalizedRestPose.hips?.position
46
+ : model.scene.getObjectByName('rest_hips')?.getWorldPosition(new Vector3()).toArray();
47
+ if (clipSceneHips == null || vrmHipsPosition == null) {
48
+ throw new Error('Failed to load animation: missing animation hips object or VRM hips position.');
49
+ }
50
+ // Adjust with reference to hips height.
51
+ const motionHipsHeight = clipSceneHips.getWorldPosition(position).y;
52
+ const [_, vrmHipsHeight] = vrmHipsPosition;
53
+ positionScale = vrmHipsHeight / motionHipsHeight;
34
54
  }
35
- // Adjust with reference to hips height.
36
- const motionHipsHeight = clipSceneHips.getWorldPosition(position).y;
37
- const [_, vrmHipsHeight] = vrmHipsPosition;
38
- const positionScale = vrmHipsHeight / motionHipsHeight;
39
55
  for (const track of clip.tracks) {
40
56
  // Convert each tracks for VRM use, and push to `tracks`
41
- const [boneName, propertyName] = track.name.split('.');
42
- const vrmBoneName = boneMap?.[boneName] ?? boneName;
43
- const targetBone = model instanceof VRM
44
- ? model.humanoid.getNormalizedBoneNode(vrmBoneName)
45
- : model.scene.getObjectByName(vrmBoneName);
46
- if (targetBone == null) {
57
+ const [clipBoneName, propertyName] = track.name.split('.');
58
+ const vrmBoneName = (boneMap?.[clipBoneName] ?? clipBoneName);
59
+ const targetNormalizedBoneName = model instanceof VRM ? model.humanoid.getNormalizedBoneNode(vrmBoneName)?.name : vrmBoneName;
60
+ if (targetNormalizedBoneName == null) {
47
61
  continue;
48
62
  }
49
- const vrmNodeName = model instanceof VRM ? targetBone.name : vrmBoneName;
50
- const baseBone = clipScene.getObjectByName(boneName);
51
- if (vrmNodeName == null || baseBone == null) {
63
+ const trackName = `${targetNormalizedBoneName}.${propertyName}`;
64
+ let targetLocalBoneTransform;
65
+ let targetParentWorldBoneTransform;
66
+ //for vrm targetLocalBoneTransform and targetParentWorldBoneTransform are the identity quaternion
67
+ if (model instanceof VRM) {
68
+ targetLocalBoneTransform = { rotation: [0, 0, 0, 1] };
69
+ targetParentWorldBoneTransform = { rotation: [0, 0, 0, 1] };
70
+ }
71
+ else {
72
+ const targetBone = model.scene.getObjectByName(`rest_${vrmBoneName}`);
73
+ if (targetBone != null) {
74
+ targetLocalBoneTransform = { rotation: targetBone.quaternion.toArray() };
75
+ }
76
+ if (targetBone?.parent != null) {
77
+ targetParentWorldBoneTransform = { rotation: targetBone.parent.getWorldQuaternion(new Quaternion()).toArray() };
78
+ }
79
+ }
80
+ if (targetLocalBoneTransform == null) {
81
+ continue;
82
+ }
83
+ let baseBone = clipScene?.getObjectByName(clipBoneName);
84
+ if (clipScene != null && baseBone == null) {
52
85
  continue;
53
86
  }
54
87
  if (track instanceof QuaternionKeyframeTrack) {
55
88
  // Store rotations of rest-pose.
56
- baseThisLocalRestRotation_inverse.copy(baseBone.quaternion).invert();
57
- if (baseBone.parent != null) {
89
+ if (baseBone != null) {
90
+ baseThisLocalRestRotation_inverse.copy(baseBone.quaternion).invert();
91
+ }
92
+ else {
93
+ baseThisLocalRestRotation_inverse.identity();
94
+ }
95
+ if (baseBone?.parent != null) {
58
96
  baseBone.parent.getWorldQuaternion(baseParentWorldRestRotation);
59
97
  baseParentWorldRestRotation_inverse.copy(baseParentWorldRestRotation).invert();
60
98
  }
@@ -62,9 +100,9 @@ export function fixModelAnimationClip(model, clip, clipScene, removeXZMovement,
62
100
  baseParentWorldRestRotation.identity();
63
101
  baseParentWorldRestRotation_inverse.identity();
64
102
  }
65
- targetThisLocalRestRotation.copy(targetBone.quaternion);
66
- if (targetBone.parent != null) {
67
- targetBone.parent.getWorldQuaternion(targetParentWorldRestRotation);
103
+ targetThisLocalRestRotation.fromArray(targetLocalBoneTransform.rotation ?? [0, 0, 0, 1]);
104
+ if (targetParentWorldBoneTransform != null) {
105
+ targetParentWorldRestRotation.fromArray(targetParentWorldBoneTransform.rotation ?? [0, 0, 0, 1]);
68
106
  targetParentWorldRestRotation_inverse.copy(targetParentWorldRestRotation).invert();
69
107
  }
70
108
  else {
@@ -90,16 +128,18 @@ export function fixModelAnimationClip(model, clip, clipScene, removeXZMovement,
90
128
  }
91
129
  targetThisLocalCurrentRotation.toArray(track.values, i);
92
130
  }
93
- track.name = `${vrmNodeName}.${propertyName}`;
131
+ track.name = trackName;
94
132
  }
95
133
  else if (track instanceof VectorKeyframeTrack) {
96
- track.name = `${vrmNodeName}.${propertyName}`;
134
+ if (vrmBoneName != 'hips' && vrmBoneName != 'root') {
135
+ continue;
136
+ }
97
137
  if (propertyName != 'position') {
98
138
  continue;
99
139
  }
100
140
  for (let i = 0; i < track.values.length; i += 3) {
101
141
  position.fromArray(track.values, i);
102
- if (clipSceneHips.parent != null) {
142
+ if (clipSceneHips?.parent != null) {
103
143
  if (vrmBoneName === 'hips') {
104
144
  position.applyMatrix4(clipSceneHips.parent.matrixWorld);
105
145
  }
@@ -108,6 +148,9 @@ export function fixModelAnimationClip(model, clip, clipScene, removeXZMovement,
108
148
  }
109
149
  }
110
150
  position.multiplyScalar(positionScale);
151
+ if (!(model instanceof VRM) && vrmBoneName === 'hips') {
152
+ position.applyQuaternion(nonVrmRotationOffset);
153
+ }
111
154
  if (model instanceof VRM) {
112
155
  if (model.meta.metaVersion === '0') {
113
156
  position.negate();
@@ -120,8 +163,12 @@ export function fixModelAnimationClip(model, clip, clipScene, removeXZMovement,
120
163
  }
121
164
  position.toArray(track.values, i);
122
165
  }
166
+ track.name = trackName;
123
167
  }
124
168
  }
169
+ if (restRoot != null && restRootParent != null) {
170
+ restRoot.parent = restRootParent;
171
+ }
125
172
  }
126
173
  export * from './gltf.js';
127
174
  export * from './fbx.js';
@@ -129,6 +176,24 @@ export * from './vrma.js';
129
176
  export * from './utils.js';
130
177
  async function uncachedLoadModelAnimation(model, type, url, removeXZMovement, trimStartTime, trimEndTime, boneMap, scaleTime) {
131
178
  let clips;
179
+ if (type == null) {
180
+ const lowerCaseUrl = url.toLocaleLowerCase();
181
+ if (lowerCaseUrl.endsWith('.glb') || lowerCaseUrl.endsWith('.gltf')) {
182
+ type = 'gltf';
183
+ }
184
+ if (lowerCaseUrl.endsWith('.fbx')) {
185
+ type = 'fbx';
186
+ }
187
+ if (lowerCaseUrl.endsWith('.bvh')) {
188
+ type = 'bvh';
189
+ }
190
+ if (lowerCaseUrl.endsWith('.vrma')) {
191
+ type = 'vrma';
192
+ }
193
+ if (type == null) {
194
+ throw new Error(`Unable to infer animation type from url "${url}. Please specify the type of the animation manually."`);
195
+ }
196
+ }
132
197
  switch (type) {
133
198
  case 'gltf':
134
199
  clips = await loadVrmModelGltfAnimations(model, url, removeXZMovement, boneMap);
@@ -136,6 +201,9 @@ async function uncachedLoadModelAnimation(model, type, url, removeXZMovement, tr
136
201
  case 'fbx':
137
202
  clips = await loadVrmModelFbxAnimations(model, url, removeXZMovement, boneMap);
138
203
  break;
204
+ case 'bvh':
205
+ clips = await loadVrmModelBvhAnimations(model, url, removeXZMovement, boneMap ?? bvhBoneMap);
206
+ break;
139
207
  case 'mixamo':
140
208
  clips = await loadVrmModelFbxAnimations(model, url, removeXZMovement, boneMap ?? mixamoBoneMap);
141
209
  break;
@@ -193,3 +261,4 @@ export async function getSimpleCharacterModelAnimationOptions(animationName) {
193
261
  };
194
262
  }
195
263
  export const mixamoBoneMap = _mixamoBoneMap;
264
+ export const bvhBoneMap = _bvhBoneMap;
@@ -6,12 +6,23 @@ export { VRMHumanBoneName } from '@pixiv/three-vrm';
6
6
  export * from './vrm.js';
7
7
  async function uncachedLoadCharacterModel(type, url, boneRotationOffset, castShadow = true, receiveShadow = true) {
8
8
  let result;
9
- if (type == null || url == null) {
9
+ if (url == null) {
10
10
  //prepare loading the default model
11
11
  type = 'gltf';
12
12
  url = (await import('../assets/mannequin.js')).url;
13
13
  boneRotationOffset = new Quaternion().setFromEuler(new Euler(Math.PI, 0, Math.PI / 2, 'ZYX'));
14
14
  }
15
+ if (type == null) {
16
+ if (url.endsWith('.gltf') || url.endsWith('.glb')) {
17
+ type = 'gltf';
18
+ }
19
+ if (url.endsWith('.vrm')) {
20
+ type = 'vrm';
21
+ }
22
+ if (type == null) {
23
+ throw new Error(`Unable to infer model type from url "${url}. Please specify the type of the model manually."`);
24
+ }
25
+ }
15
26
  switch (type) {
16
27
  case 'vrm':
17
28
  result = await loadVrmCharacterModel(url);
@@ -30,6 +41,14 @@ async function uncachedLoadCharacterModel(type, url, boneRotationOffset, castSha
30
41
  obj.receiveShadow = true;
31
42
  }
32
43
  });
44
+ const rootBone = result.scene.getObjectByName('root');
45
+ if (rootBone == null) {
46
+ throw new Error(`unable to load model - missing root bone`);
47
+ }
48
+ const restPose = rootBone.clone();
49
+ restPose.visible = false;
50
+ restPose.traverse((bone) => (bone.name = `rest_${bone.name}`));
51
+ result.scene.add(restPose);
33
52
  return result;
34
53
  }
35
54
  function getCharacterModelDependencies(options = true) {
@@ -98,6 +98,7 @@ export declare class SimpleCharacter extends Group<Object3DEventMap & {
98
98
  actions?: Record<(typeof simpleCharacterAnimationNames)[number], AnimationAction> | undefined;
99
99
  model?: Awaited<Exclude<ReturnType<typeof loadCharacterModel>, undefined>>;
100
100
  private updateTimeline?;
101
+ private readonly abortController;
101
102
  constructor(camera: Object3D | (() => Object3D), world: BvhPhysicsWorld, domElement: HTMLElement, options?: SimpleCharacterOptions);
102
103
  getCamera(): Object3D<Object3DEventMap>;
103
104
  private init;
@@ -182,8 +182,8 @@ async function* SimpleCharacterTimeline(character) {
182
182
  init: () => {
183
183
  actions.jumpForward.paused = false;
184
184
  applyJumpForce();
185
+ return () => actions.jumpForward.fadeOut(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration);
185
186
  },
186
- cleanup: () => void actions.jumpForward.fadeOut(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration),
187
187
  until: animationFinished(actions.jumpForward),
188
188
  });
189
189
  if (character.physics.isGrounded) {
@@ -197,13 +197,13 @@ async function* SimpleCharacterTimeline(character) {
197
197
  init: () => {
198
198
  actions.jumpUp.paused = false;
199
199
  applyJumpForce();
200
+ return () => void actions.jumpUp.fadeOut(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration);
200
201
  },
201
- cleanup: () => void actions.jumpUp.fadeOut(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration),
202
202
  until: animationFinished(actions.jumpUp),
203
203
  }),
204
204
  transitionTo: {
205
205
  jumpDown: {
206
- when: (_, clock) => clock.actionTime > 0.3 && character.physics.isGrounded,
206
+ when: (_, _clock, actionTime) => actionTime > 0.3 && character.physics.isGrounded,
207
207
  },
208
208
  finally: 'jumpLoop',
209
209
  },
@@ -214,8 +214,8 @@ async function* SimpleCharacterTimeline(character) {
214
214
  actions.jumpLoop.reset();
215
215
  actions.jumpLoop.play();
216
216
  actions.jumpLoop.fadeIn(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration);
217
+ return () => actions.jumpLoop.fadeOut(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration);
217
218
  },
218
- cleanup: () => actions.jumpLoop.fadeOut(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration),
219
219
  until: forever(),
220
220
  }),
221
221
  transitionTo: {
@@ -230,8 +230,8 @@ async function* SimpleCharacterTimeline(character) {
230
230
  actions.jumpDown.reset();
231
231
  actions.jumpDown.play();
232
232
  actions.jumpDown.fadeIn(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration);
233
+ return () => actions.jumpDown.fadeOut(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration);
233
234
  },
234
- cleanup: () => actions.jumpDown.fadeOut(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration),
235
235
  until: timePassed(150, 'milliseconds'),
236
236
  }),
237
237
  transitionTo: { finally: 'moving' },
@@ -263,7 +263,7 @@ async function* SimpleCharacterTimeline(character) {
263
263
  nextAnimation.fadeIn(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration);
264
264
  currentAnimation = nextAnimation;
265
265
  },
266
- cleanup: () => currentAnimation?.fadeOut(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration),
266
+ init: () => () => currentAnimation?.fadeOut(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration),
267
267
  });
268
268
  },
269
269
  transitionTo: {
@@ -285,6 +285,7 @@ export class SimpleCharacter extends Group {
285
285
  actions;
286
286
  model;
287
287
  updateTimeline;
288
+ abortController = new AbortController();
288
289
  constructor(camera, world, domElement, options = {}) {
289
290
  super();
290
291
  this.camera = camera;
@@ -320,7 +321,7 @@ export class SimpleCharacter extends Group {
320
321
  this.actions.jumpForward.loop = LoopOnce;
321
322
  this.actions.jumpForward.clampWhenFinished = true;
322
323
  }
323
- this.updateTimeline = start(SimpleCharacterTimeline(this));
324
+ this.updateTimeline = start(SimpleCharacterTimeline(this), this.abortController.signal);
324
325
  this.dispatchEvent({ type: 'loaded' });
325
326
  }
326
327
  update(delta) {
@@ -333,6 +334,7 @@ export class SimpleCharacter extends Group {
333
334
  this.cameraBehavior.update(delta, this.options.cameraBehavior);
334
335
  }
335
336
  dispose() {
337
+ this.abortController.abort();
336
338
  this.parent?.remove(this);
337
339
  this.model?.scene.dispatchEvent({ type: 'dispose' });
338
340
  this.inputSystem.dispose();
package/package.json CHANGED
@@ -21,12 +21,12 @@
21
21
  "peerDependencies": {
22
22
  "three": "*"
23
23
  },
24
- "version": "0.1.19",
24
+ "version": "0.1.20",
25
25
  "type": "module",
26
26
  "dependencies": {
27
27
  "@pixiv/three-vrm": "^3.4.2",
28
28
  "@pixiv/three-vrm-animation": "^3.4.2",
29
- "@pmndrs/timeline": "^0.1.15",
29
+ "@pmndrs/timeline": "^0.2.6",
30
30
  "@viverse/sdk": "1.2.10-alpha.0",
31
31
  "three-mesh-bvh": "^0.9.1"
32
32
  },