@pmndrs/viverse 0.1.20 → 0.2.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 (67) hide show
  1. package/dist/animation/bone-map.d.ts +4 -0
  2. package/dist/animation/bone-map.js +11 -0
  3. package/dist/animation/default.d.ts +9 -0
  4. package/dist/animation/default.js +25 -0
  5. package/dist/animation/index.d.ts +12 -19
  6. package/dist/animation/index.js +76 -72
  7. package/dist/animation/mask.d.ts +6 -0
  8. package/dist/animation/mask.js +53 -0
  9. package/dist/camera.d.ts +7 -7
  10. package/dist/camera.js +33 -26
  11. package/dist/index.d.ts +3 -2
  12. package/dist/index.js +3 -2
  13. package/dist/input/action.d.ts +41 -0
  14. package/dist/input/action.js +97 -0
  15. package/dist/input/index.d.ts +12 -27
  16. package/dist/input/index.js +17 -69
  17. package/dist/input/keyboard.d.ts +29 -7
  18. package/dist/input/keyboard.js +84 -72
  19. package/dist/input/pointer-capture.d.ts +6 -11
  20. package/dist/input/pointer-capture.js +10 -30
  21. package/dist/input/pointer-lock.d.ts +6 -11
  22. package/dist/input/pointer-lock.js +8 -29
  23. package/dist/input/screen-joystick.d.ts +12 -11
  24. package/dist/input/screen-joystick.js +39 -49
  25. package/dist/input/screen-jump-button.d.ts +2 -4
  26. package/dist/input/screen-jump-button.js +7 -12
  27. package/dist/model/index.d.ts +9 -13
  28. package/dist/model/index.js +12 -34
  29. package/dist/physics/index.d.ts +2 -5
  30. package/dist/physics/index.js +7 -16
  31. package/dist/simple-character/apply-input-options.d.ts +2 -0
  32. package/dist/simple-character/apply-input-options.js +28 -0
  33. package/dist/simple-character/defaults.d.ts +2 -0
  34. package/dist/simple-character/defaults.js +2 -0
  35. package/dist/simple-character/index.d.ts +116 -0
  36. package/dist/simple-character/index.js +104 -0
  37. package/dist/simple-character/state/index.d.ts +6 -0
  38. package/dist/simple-character/state/index.js +6 -0
  39. package/dist/simple-character/state/jump-down.d.ts +3 -0
  40. package/dist/simple-character/state/jump-down.js +26 -0
  41. package/dist/simple-character/state/jump-forward.d.ts +5 -0
  42. package/dist/simple-character/state/jump-forward.js +41 -0
  43. package/dist/simple-character/state/jump-loop.d.ts +3 -0
  44. package/dist/simple-character/state/jump-loop.js +24 -0
  45. package/dist/simple-character/state/jump-start.d.ts +4 -0
  46. package/dist/simple-character/state/jump-start.js +30 -0
  47. package/dist/simple-character/state/jump-up.d.ts +5 -0
  48. package/dist/simple-character/state/jump-up.js +40 -0
  49. package/dist/simple-character/state/movement.d.ts +3 -0
  50. package/dist/simple-character/state/movement.js +63 -0
  51. package/dist/simple-character/update-input-velocity.d.ts +4 -0
  52. package/dist/simple-character/update-input-velocity.js +25 -0
  53. package/dist/simple-character/update-rotation.d.ts +6 -0
  54. package/dist/simple-character/update-rotation.js +40 -0
  55. package/dist/utils.d.ts +12 -5
  56. package/dist/utils.js +37 -39
  57. package/package.json +2 -2
  58. package/dist/animation/bvh.d.ts +0 -4
  59. package/dist/animation/bvh.js +0 -8
  60. package/dist/animation/fbx.d.ts +0 -4
  61. package/dist/animation/fbx.js +0 -8
  62. package/dist/animation/gltf.d.ts +0 -3
  63. package/dist/animation/gltf.js +0 -8
  64. package/dist/animation/vrma.d.ts +0 -3
  65. package/dist/animation/vrma.js +0 -8
  66. package/dist/simple-character.d.ts +0 -107
  67. package/dist/simple-character.js +0 -344
@@ -0,0 +1,25 @@
1
+ export const IdleAnimationUrl = Symbol('idle-animation-url');
2
+ export const JumpUpAnimationUrl = Symbol('jump-up-animation-url');
3
+ export const JumpDownAnimationUrl = Symbol('jump-down-animation-url');
4
+ export const JumpForwardAnimationUrl = Symbol('jump-forward-animation-url');
5
+ export const JumpLoopAnimationUrl = Symbol('jump-loop-animation-url');
6
+ export const RunAnimationUrl = Symbol('run-animation-url');
7
+ export const WalkAnimationUrl = Symbol('walk-animation-url');
8
+ export async function resolveDefaultCharacterAnimationUrl(url) {
9
+ switch (url) {
10
+ case IdleAnimationUrl:
11
+ return (await import('../assets/idle.js')).url;
12
+ case JumpDownAnimationUrl:
13
+ return (await import('../assets/jump-down.js')).url;
14
+ case JumpForwardAnimationUrl:
15
+ return (await import('../assets/jump-forward.js')).url;
16
+ case JumpLoopAnimationUrl:
17
+ return (await import('../assets/jump-loop.js')).url;
18
+ case JumpUpAnimationUrl:
19
+ return (await import('../assets/jump-up.js')).url;
20
+ case RunAnimationUrl:
21
+ return (await import('../assets/run.js')).url;
22
+ case WalkAnimationUrl:
23
+ return (await import('../assets/walk.js')).url;
24
+ }
25
+ }
@@ -1,14 +1,15 @@
1
1
  import { VRMHumanBoneName } from '@pixiv/three-vrm';
2
2
  import { AnimationClip, Object3D } from 'three';
3
- import { loadCharacterModel } from '../model/index.js';
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
- export * from './gltf.js';
6
- export * from './fbx.js';
7
- export * from './vrma.js';
3
+ import { DefaultUrl } from './default.js';
4
+ import { type CharacterAnimationMask } from './mask.js';
5
+ import { type CharacterModel } from '../model/index.js';
6
+ export declare function fixModelAnimationClip(model: CharacterModel, clip: AnimationClip, clipScene: Object3D | undefined, removeXZMovement: boolean): void;
8
7
  export * from './utils.js';
9
- export type ModelAnimationOptions = {
8
+ export * from './default.js';
9
+ export * from './mask.js';
10
+ export type CharacterAnimationOptions = {
11
+ url: string | DefaultUrl;
10
12
  type?: 'mixamo' | 'gltf' | 'vrma' | 'fbx' | 'bvh';
11
- url: string;
12
13
  removeXZMovement?: boolean;
13
14
  trimTime?: {
14
15
  start?: number;
@@ -16,18 +17,10 @@ export type ModelAnimationOptions = {
16
17
  };
17
18
  boneMap?: Record<string, VRMHumanBoneName>;
18
19
  scaleTime?: number;
20
+ mask?: CharacterAnimationMask;
19
21
  };
20
- export declare function loadCharacterModelAnimation(model: Exclude<Awaited<ReturnType<typeof loadCharacterModel>>, undefined>, options: ModelAnimationOptions): Promise<AnimationClip>;
21
- declare const simpleCharacterAnimationUrls: {
22
- walk: () => Promise<typeof import("../assets/walk.js")>;
23
- run: () => Promise<typeof import("../assets/run.js")>;
24
- idle: () => Promise<typeof import("../assets/idle.js")>;
25
- jumpUp: () => Promise<typeof import("../assets/jump-up.js")>;
26
- jumpLoop: () => Promise<typeof import("../assets/jump-loop.js")>;
27
- jumpDown: () => Promise<typeof import("../assets/jump-down.js")>;
28
- jumpForward: () => Promise<typeof import("../assets/jump-forward.js")>;
29
- };
30
- export declare const simpleCharacterAnimationNames: Array<keyof typeof simpleCharacterAnimationUrls>;
31
- export declare function getSimpleCharacterModelAnimationOptions(animationName: keyof typeof simpleCharacterAnimationUrls): Promise<ModelAnimationOptions>;
22
+ export type Tail<T extends any[]> = T extends [any, ...infer Rest] ? Rest : never;
23
+ export declare function flattenCharacterAnimationOptions(options: Exclude<CharacterAnimationOptions, false>): Tail<Parameters<typeof loadCharacterAnimation>>;
24
+ export declare function loadCharacterAnimation(model: CharacterModel, url: string | DefaultUrl, type?: CharacterAnimationOptions['type'], removeXZMovement?: boolean, trimStartTime?: number | undefined, trimEndTime?: number | undefined, boneMap?: Record<string, VRMHumanBoneName> | undefined, scaleTime?: number | undefined, mask?: CharacterAnimationMask): Promise<AnimationClip>;
32
25
  export declare const mixamoBoneMap: Record<string, VRMHumanBoneName>;
33
26
  export declare const bvhBoneMap: Record<string, VRMHumanBoneName>;
@@ -1,13 +1,15 @@
1
1
  import { VRM } from '@pixiv/three-vrm';
2
2
  import { Euler, Quaternion, QuaternionKeyframeTrack, Vector3, VectorKeyframeTrack, } from 'three';
3
+ import { BVHLoader } from 'three/examples/jsm/loaders/BVHLoader.js';
4
+ import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js';
5
+ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
6
+ import { applyBoneMap } from './bone-map.js';
3
7
  import _bvhBoneMap from './bvh-bone-map.json';
4
- import { loadVrmModelBvhAnimations } from './bvh.js';
5
- import { loadVrmModelFbxAnimations } from './fbx.js';
6
- import { loadVrmModelGltfAnimations } from './gltf.js';
8
+ import { resolveDefaultCharacterAnimationUrl } from './default.js';
9
+ import { applyMask } from './mask.js';
7
10
  import _mixamoBoneMap from './mixamo-bone-map.json';
8
11
  import { scaleAnimationClipTime, trimAnimationClip } from './utils.js';
9
- import { loadVrmModelVrmaAnimations } from './vrma.js';
10
- import { cached } from '../utils.js';
12
+ import { vrmaLoader } from '../model/index.js';
11
13
  //helper variables for the quaternion retargeting
12
14
  const baseThisLocalRestRotation_inverse = new Quaternion();
13
15
  const baseThisLocalCurrentRotation = new Quaternion();
@@ -21,11 +23,7 @@ const targetThisLocalCurrentRotation = new Quaternion();
21
23
  const position = new Vector3();
22
24
  const nonVrmRotationOffset = new Quaternion().setFromEuler(new Euler(0, Math.PI, 0));
23
25
  //TODO: currently assumes the model is not yet transformed - loaded for the first time
24
- export function fixModelAnimationClip(model, clip, clipScene, removeXZMovement, boneMap) {
25
- const hipsClipBoneName = boneMap == null ? 'hips' : Object.entries(boneMap).find(([, vrmBoneName]) => vrmBoneName === 'hips')?.[0];
26
- if (hipsClipBoneName == null) {
27
- throw new Error('Failed to determine hips bone name for VRM animation. Please check the bone map or animation file.');
28
- }
26
+ export function fixModelAnimationClip(model, clip, clipScene, removeXZMovement) {
29
27
  let restRoot;
30
28
  let restRootParent;
31
29
  if (!(model instanceof VRM)) {
@@ -39,24 +37,22 @@ export function fixModelAnimationClip(model, clip, clipScene, removeXZMovement,
39
37
  let positionScale = 1;
40
38
  let clipSceneHips;
41
39
  if (clipScene != null) {
42
- clipSceneHips = clipScene.getObjectByName(hipsClipBoneName);
40
+ clipSceneHips = clipScene.getObjectByName('hips');
43
41
  clipSceneHips?.parent?.updateMatrixWorld();
44
42
  const vrmHipsPosition = model instanceof VRM
45
43
  ? model.humanoid.normalizedRestPose.hips?.position
46
44
  : 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.');
45
+ if (clipSceneHips != null && vrmHipsPosition != null) {
46
+ // Adjust with reference to hips height.
47
+ const motionHipsHeight = clipSceneHips.getWorldPosition(position).y;
48
+ const [_, vrmHipsHeight] = vrmHipsPosition;
49
+ positionScale = vrmHipsHeight / motionHipsHeight;
49
50
  }
50
- // Adjust with reference to hips height.
51
- const motionHipsHeight = clipSceneHips.getWorldPosition(position).y;
52
- const [_, vrmHipsHeight] = vrmHipsPosition;
53
- positionScale = vrmHipsHeight / motionHipsHeight;
54
51
  }
55
52
  for (const track of clip.tracks) {
56
53
  // Convert each tracks for VRM use, and push to `tracks`
57
54
  const [clipBoneName, propertyName] = track.name.split('.');
58
- const vrmBoneName = (boneMap?.[clipBoneName] ?? clipBoneName);
59
- const targetNormalizedBoneName = model instanceof VRM ? model.humanoid.getNormalizedBoneNode(vrmBoneName)?.name : vrmBoneName;
55
+ const targetNormalizedBoneName = model instanceof VRM ? model.humanoid.getNormalizedBoneNode(clipBoneName)?.name : clipBoneName;
60
56
  if (targetNormalizedBoneName == null) {
61
57
  continue;
62
58
  }
@@ -69,7 +65,7 @@ export function fixModelAnimationClip(model, clip, clipScene, removeXZMovement,
69
65
  targetParentWorldBoneTransform = { rotation: [0, 0, 0, 1] };
70
66
  }
71
67
  else {
72
- const targetBone = model.scene.getObjectByName(`rest_${vrmBoneName}`);
68
+ const targetBone = model.scene.getObjectByName(`rest_${clipBoneName}`);
73
69
  if (targetBone != null) {
74
70
  targetLocalBoneTransform = { rotation: targetBone.quaternion.toArray() };
75
71
  }
@@ -123,7 +119,7 @@ export function fixModelAnimationClip(model, clip, clipScene, removeXZMovement,
123
119
  targetThisLocalCurrentRotation.x *= -1;
124
120
  targetThisLocalCurrentRotation.z *= -1;
125
121
  }
126
- if (!(model instanceof VRM) && vrmBoneName === 'hips') {
122
+ if (!(model instanceof VRM) && clipBoneName === 'hips') {
127
123
  targetThisLocalCurrentRotation.premultiply(nonVrmRotationOffset);
128
124
  }
129
125
  targetThisLocalCurrentRotation.toArray(track.values, i);
@@ -131,7 +127,7 @@ export function fixModelAnimationClip(model, clip, clipScene, removeXZMovement,
131
127
  track.name = trackName;
132
128
  }
133
129
  else if (track instanceof VectorKeyframeTrack) {
134
- if (vrmBoneName != 'hips' && vrmBoneName != 'root') {
130
+ if (clipBoneName != 'hips' && clipBoneName != 'root') {
135
131
  continue;
136
132
  }
137
133
  if (propertyName != 'position') {
@@ -140,7 +136,7 @@ export function fixModelAnimationClip(model, clip, clipScene, removeXZMovement,
140
136
  for (let i = 0; i < track.values.length; i += 3) {
141
137
  position.fromArray(track.values, i);
142
138
  if (clipSceneHips?.parent != null) {
143
- if (vrmBoneName === 'hips') {
139
+ if (clipBoneName === 'hips') {
144
140
  position.applyMatrix4(clipSceneHips.parent.matrixWorld);
145
141
  }
146
142
  else {
@@ -148,7 +144,7 @@ export function fixModelAnimationClip(model, clip, clipScene, removeXZMovement,
148
144
  }
149
145
  }
150
146
  position.multiplyScalar(positionScale);
151
- if (!(model instanceof VRM) && vrmBoneName === 'hips') {
147
+ if (!(model instanceof VRM) && clipBoneName === 'hips') {
152
148
  position.applyQuaternion(nonVrmRotationOffset);
153
149
  }
154
150
  if (model instanceof VRM) {
@@ -157,7 +153,7 @@ export function fixModelAnimationClip(model, clip, clipScene, removeXZMovement,
157
153
  position.y *= -1;
158
154
  }
159
155
  }
160
- if (vrmBoneName === 'hips' && removeXZMovement) {
156
+ if (clipBoneName === 'hips' && removeXZMovement) {
161
157
  position.x = 0;
162
158
  position.z = 0;
163
159
  }
@@ -170,12 +166,32 @@ export function fixModelAnimationClip(model, clip, clipScene, removeXZMovement,
170
166
  restRoot.parent = restRootParent;
171
167
  }
172
168
  }
173
- export * from './gltf.js';
174
- export * from './fbx.js';
175
- export * from './vrma.js';
176
169
  export * from './utils.js';
177
- async function uncachedLoadModelAnimation(model, type, url, removeXZMovement, trimStartTime, trimEndTime, boneMap, scaleTime) {
170
+ export * from './default.js';
171
+ export * from './mask.js';
172
+ export function flattenCharacterAnimationOptions(options) {
173
+ return [
174
+ options.url,
175
+ options.type,
176
+ options.removeXZMovement,
177
+ options.trimTime?.start,
178
+ options.trimTime?.end,
179
+ options.boneMap,
180
+ options.scaleTime,
181
+ options.mask,
182
+ ];
183
+ }
184
+ const gltfLoader = new GLTFLoader();
185
+ const fbxLoader = new FBXLoader();
186
+ const bvhLoader = new BVHLoader();
187
+ export async function loadCharacterAnimation(model, url, type, removeXZMovement = false, trimStartTime, trimEndTime, boneMap, scaleTime, mask) {
188
+ if (typeof url === 'symbol') {
189
+ url = await resolveDefaultCharacterAnimationUrl(url);
190
+ type = 'gltf';
191
+ }
178
192
  let clips;
193
+ let clipScene;
194
+ let defaultBoneMap;
179
195
  if (type == null) {
180
196
  const lowerCaseUrl = url.toLocaleLowerCase();
181
197
  if (lowerCaseUrl.endsWith('.glb') || lowerCaseUrl.endsWith('.gltf')) {
@@ -195,29 +211,51 @@ async function uncachedLoadModelAnimation(model, type, url, removeXZMovement, tr
195
211
  }
196
212
  }
197
213
  switch (type) {
198
- case 'gltf':
199
- clips = await loadVrmModelGltfAnimations(model, url, removeXZMovement, boneMap);
214
+ case 'gltf': {
215
+ const { animations, scene } = await gltfLoader.loadAsync(url);
216
+ clips = animations;
217
+ clipScene = scene;
200
218
  break;
201
- case 'fbx':
202
- clips = await loadVrmModelFbxAnimations(model, url, removeXZMovement, boneMap);
219
+ }
220
+ case 'fbx': {
221
+ const scene = await fbxLoader.loadAsync(url);
222
+ clips = scene.animations;
223
+ clipScene = scene;
203
224
  break;
204
- case 'bvh':
205
- clips = await loadVrmModelBvhAnimations(model, url, removeXZMovement, boneMap ?? bvhBoneMap);
225
+ }
226
+ case 'bvh': {
227
+ const { clip, skeleton } = await bvhLoader.loadAsync(url);
228
+ clips = [clip];
229
+ boneMap ??= bvhBoneMap;
206
230
  break;
207
- case 'mixamo':
208
- clips = await loadVrmModelFbxAnimations(model, url, removeXZMovement, boneMap ?? mixamoBoneMap);
231
+ }
232
+ case 'mixamo': {
233
+ const scene = await fbxLoader.loadAsync(url);
234
+ clips = scene.animations;
235
+ clipScene = scene;
236
+ boneMap ??= mixamoBoneMap;
209
237
  break;
238
+ }
210
239
  case 'vrma':
211
240
  if (!(model instanceof VRM)) {
212
241
  throw new Error(`Model must be an instance of VRM to load VRMA animations`);
213
242
  }
214
- clips = await loadVrmModelVrmaAnimations(model, url, removeXZMovement);
243
+ clips = (await vrmaLoader.loadAsync(url)).userData.vrmAnimations;
215
244
  break;
216
245
  }
217
246
  if (clips.length != 1) {
218
247
  throw new Error(`Expected exactly one animation clip, but got ${clips.length} for url ${url}`);
219
248
  }
220
249
  const [clip] = clips;
250
+ if (boneMap != null) {
251
+ applyBoneMap(clip, clipScene, boneMap);
252
+ }
253
+ if (mask != null) {
254
+ applyMask(clip, mask);
255
+ }
256
+ if (type != 'vrma') {
257
+ fixModelAnimationClip(model, clip, clipScene, removeXZMovement);
258
+ }
221
259
  if (trimStartTime != null || trimEndTime != null) {
222
260
  trimAnimationClip(clip, trimStartTime, trimEndTime);
223
261
  }
@@ -226,39 +264,5 @@ async function uncachedLoadModelAnimation(model, type, url, removeXZMovement, tr
226
264
  }
227
265
  return clip;
228
266
  }
229
- export function loadCharacterModelAnimation(model, options) {
230
- return cached(uncachedLoadModelAnimation, [
231
- model,
232
- options.type,
233
- options.url,
234
- options.removeXZMovement ?? false,
235
- options.trimTime?.start,
236
- options.trimTime?.end,
237
- options.boneMap,
238
- options.scaleTime,
239
- ]);
240
- }
241
- const extraOptions = {
242
- walk: { scaleTime: 0.5 },
243
- run: { scaleTime: 0.8 },
244
- jumpForward: { scaleTime: 0.9 },
245
- };
246
- const simpleCharacterAnimationUrls = {
247
- walk: () => import('../assets/walk.js'),
248
- run: () => import('../assets/run.js'),
249
- idle: () => import('../assets/idle.js'),
250
- jumpUp: () => import('../assets/jump-up.js'),
251
- jumpLoop: () => import('../assets/jump-loop.js'),
252
- jumpDown: () => import('../assets/jump-down.js'),
253
- jumpForward: () => import('../assets/jump-forward.js'),
254
- };
255
- export const simpleCharacterAnimationNames = Object.keys(simpleCharacterAnimationUrls);
256
- export async function getSimpleCharacterModelAnimationOptions(animationName) {
257
- return {
258
- type: 'gltf',
259
- ...extraOptions[animationName],
260
- url: (await simpleCharacterAnimationUrls[animationName]()).url,
261
- };
262
- }
263
267
  export const mixamoBoneMap = _mixamoBoneMap;
264
268
  export const bvhBoneMap = _bvhBoneMap;
@@ -0,0 +1,6 @@
1
+ import type { VRMHumanBoneName } from '@pixiv/three-vrm';
2
+ import type { AnimationClip } from 'three';
3
+ export type CharacterAnimationMask = (boneName: VRMHumanBoneName) => boolean;
4
+ export declare function applyMask(clip: AnimationClip, mask: CharacterAnimationMask): void;
5
+ export declare const upperBody: CharacterAnimationMask;
6
+ export declare const lowerBody: CharacterAnimationMask;
@@ -0,0 +1,53 @@
1
+ export function applyMask(clip, mask) {
2
+ clip.tracks = clip.tracks.filter((track) => mask(track.name.split('.')[0]));
3
+ }
4
+ const upperBodyParts = [
5
+ 'spine',
6
+ 'chest',
7
+ 'upperChest',
8
+ 'neck',
9
+ 'head',
10
+ 'leftEye',
11
+ 'rightEye',
12
+ 'jaw',
13
+ 'leftShoulder',
14
+ 'leftUpperArm',
15
+ 'leftLowerArm',
16
+ 'leftHand',
17
+ 'rightShoulder',
18
+ 'rightUpperArm',
19
+ 'rightLowerArm',
20
+ 'rightHand',
21
+ 'leftThumbMetacarpal',
22
+ 'leftThumbProximal',
23
+ 'leftThumbDistal',
24
+ 'leftIndexProximal',
25
+ 'leftIndexIntermediate',
26
+ 'leftIndexDistal',
27
+ 'leftMiddleProximal',
28
+ 'leftMiddleIntermediate',
29
+ 'leftMiddleDistal',
30
+ 'leftRingProximal',
31
+ 'leftRingIntermediate',
32
+ 'leftRingDistal',
33
+ 'leftLittleProximal',
34
+ 'leftLittleIntermediate',
35
+ 'leftLittleDistal',
36
+ 'rightThumbMetacarpal',
37
+ 'rightThumbProximal',
38
+ 'rightThumbDistal',
39
+ 'rightIndexProximal',
40
+ 'rightIndexIntermediate',
41
+ 'rightIndexDistal',
42
+ 'rightMiddleProximal',
43
+ 'rightMiddleIntermediate',
44
+ 'rightMiddleDistal',
45
+ 'rightRingProximal',
46
+ 'rightRingIntermediate',
47
+ 'rightRingDistal',
48
+ 'rightLittleProximal',
49
+ 'rightLittleIntermediate',
50
+ 'rightLittleDistal',
51
+ ];
52
+ export const upperBody = (name) => upperBodyParts.includes(name);
53
+ export const lowerBody = (name) => !upperBodyParts.includes(name);
package/dist/camera.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { Object3D, Vector3, Vector3Tuple, Ray } from 'three';
2
- import { SimpleCharacter } from './simple-character.js';
3
2
  export declare const FirstPersonCharacterCameraBehavior: SimpleCharacterCameraBehaviorOptions;
4
3
  export type SimpleCharacterCameraBehaviorOptions = {
5
4
  /**
@@ -55,16 +54,16 @@ export type SimpleCharacterCameraBehaviorOptions = {
55
54
  maxDistance?: number;
56
55
  } | boolean;
57
56
  } | boolean;
58
- export declare class SimpleCharacterCameraBehavior {
59
- getCamera: () => Object3D;
60
- character: SimpleCharacter;
61
- private readonly raycast?;
57
+ export declare class CharacterCameraBehavior {
62
58
  rotationPitch: number;
63
59
  rotationYaw: number;
64
60
  zoomDistance: number;
65
61
  private collisionFreeZoomDistance;
66
62
  private firstUpdate;
67
- constructor(getCamera: () => Object3D, character: SimpleCharacter, raycast?: ((ray: Ray, far: number) => number | undefined) | undefined);
63
+ private readonly abortController;
64
+ private readonly yawReader;
65
+ private readonly pitchReader;
66
+ private readonly zoomReader;
68
67
  private setRotationFromDelta;
69
68
  private setDistanceFromDelta;
70
69
  private computeCharacterBaseOffset;
@@ -74,5 +73,6 @@ export declare class SimpleCharacterCameraBehavior {
74
73
  /**
75
74
  * @param delta in seconds
76
75
  */
77
- update(deltaTime: number, options?: SimpleCharacterCameraBehaviorOptions): void;
76
+ update(camera: Object3D, target: Object3D, deltaTime: number, raycast?: (ray: Ray, far: number) => number | undefined, options?: SimpleCharacterCameraBehaviorOptions): void;
77
+ dispose(): void;
78
78
  }
package/dist/camera.js CHANGED
@@ -1,6 +1,6 @@
1
- import { Vector3, Euler, Ray } from 'three';
1
+ import { Vector3, Euler, Ray, Quaternion } from 'three';
2
2
  import { clamp } from 'three/src/math/MathUtils.js';
3
- import { DeltaYawField, DeltaPitchField, DeltaZoomField } from './input/index.js';
3
+ import { RotatePitchAction, RotateYawAction, ZoomAction } from './input/index.js';
4
4
  export const FirstPersonCharacterCameraBehavior = {
5
5
  characterBaseOffset: [0, 1.6, 0],
6
6
  zoom: { maxDistance: 0, minDistance: 0 },
@@ -11,25 +11,22 @@ const sphericalOffset = new Vector3();
11
11
  const characterWorldPosition = new Vector3();
12
12
  const euler = new Euler();
13
13
  const rayHelper = new Ray();
14
- export class SimpleCharacterCameraBehavior {
15
- getCamera;
16
- character;
17
- raycast;
14
+ const quaternionHelper = new Quaternion();
15
+ export class CharacterCameraBehavior {
18
16
  rotationPitch = (-20 * Math.PI) / 180;
19
17
  rotationYaw = 0;
20
18
  zoomDistance = 4; // Changed from zoom to distance for clearer semantics
21
19
  //internal state
22
20
  collisionFreeZoomDistance = this.zoomDistance;
23
21
  firstUpdate = true;
24
- constructor(getCamera, character, raycast) {
25
- this.getCamera = getCamera;
26
- this.character = character;
27
- this.raycast = raycast;
28
- }
29
- setRotationFromDelta(delta, rotationOptions) {
22
+ abortController = new AbortController();
23
+ yawReader = RotateYawAction.createReader(this.abortController.signal);
24
+ pitchReader = RotatePitchAction.createReader(this.abortController.signal);
25
+ zoomReader = ZoomAction.createReader(this.abortController.signal);
26
+ setRotationFromDelta(camera, delta, rotationOptions) {
30
27
  if (delta.lengthSq() < 0.0001) {
31
28
  // use current camera rotation if very close to target
32
- euler.setFromQuaternion(this.getCamera().quaternion, 'YXZ');
29
+ euler.setFromQuaternion(camera.quaternion, 'YXZ');
33
30
  this.rotationPitch = euler.x;
34
31
  this.rotationYaw = euler.y;
35
32
  return;
@@ -63,7 +60,7 @@ export class SimpleCharacterCameraBehavior {
63
60
  /**
64
61
  * @param delta in seconds
65
62
  */
66
- update(deltaTime, options = true) {
63
+ update(camera, target, deltaTime, raycast, options = true) {
67
64
  if (options === false) {
68
65
  this.firstUpdate = true;
69
66
  return;
@@ -74,33 +71,38 @@ export class SimpleCharacterCameraBehavior {
74
71
  }
75
72
  //compute character->camera delta through offset
76
73
  this.computeCharacterBaseOffset(chracterBaseOffsetHelper, options.characterBaseOffset);
77
- this.character.getWorldPosition(characterWorldPosition);
74
+ target.getWorldQuaternion(quaternionHelper);
75
+ chracterBaseOffsetHelper.applyQuaternion(quaternionHelper);
76
+ target.getWorldPosition(characterWorldPosition);
78
77
  characterWorldPosition.add(chracterBaseOffsetHelper);
79
- this.getCamera().getWorldPosition(deltaHelper);
78
+ camera.getWorldPosition(deltaHelper);
80
79
  deltaHelper.sub(characterWorldPosition);
81
80
  // apply rotation input to rotationYaw and rotationPitch if not disabled or first update
82
81
  let rotationOptions = options.rotation ?? true;
83
82
  if (!this.firstUpdate && rotationOptions !== false) {
84
83
  rotationOptions = rotationOptions === true ? {} : rotationOptions;
85
84
  const rotationSpeed = rotationOptions.speed ?? 1000.0;
86
- const deltaYaw = this.character.inputSystem.get(DeltaYawField);
87
- const deltaPitch = this.character.inputSystem.get(DeltaPitchField);
85
+ this.yawReader.update();
86
+ this.pitchReader.update();
87
+ const deltaYaw = this.yawReader.get();
88
+ const deltaPitch = this.pitchReader.get();
88
89
  this.rotationYaw = this.clampYaw(this.rotationYaw + deltaYaw * rotationSpeed * deltaTime, rotationOptions);
89
90
  this.rotationPitch = this.clampPitch(this.rotationPitch + deltaPitch * rotationSpeed * deltaTime, rotationOptions);
90
91
  }
91
92
  else {
92
- this.setRotationFromDelta(deltaHelper, typeof rotationOptions === 'boolean' ? {} : rotationOptions);
93
+ this.setRotationFromDelta(camera, deltaHelper, typeof rotationOptions === 'boolean' ? {} : rotationOptions);
93
94
  }
94
95
  // apply yaw and pitch to camera rotation
95
- this.getCamera().rotation.set(this.rotationPitch, this.rotationYaw, 0, 'YXZ');
96
- rayHelper.direction.set(0, 0, 1).applyEuler(this.getCamera().rotation);
96
+ camera.rotation.set(this.rotationPitch, this.rotationYaw, 0, 'YXZ');
97
+ rayHelper.direction.set(0, 0, 1).applyEuler(camera.rotation);
97
98
  rayHelper.origin.copy(characterWorldPosition);
98
99
  // apply zoom input to zoomDistance if not disabled or first update
99
100
  let zoomOptions = options.zoom ?? true;
100
101
  if (!this.firstUpdate && zoomOptions !== false) {
101
102
  zoomOptions = zoomOptions === true ? {} : zoomOptions;
102
103
  const zoomSpeed = zoomOptions.speed ?? 1000.0;
103
- const deltaZoom = this.character.inputSystem.get(DeltaZoomField);
104
+ this.zoomReader.update();
105
+ const deltaZoom = this.zoomReader.get();
104
106
  const zoomFactor = 1 + deltaZoom * zoomSpeed * deltaTime;
105
107
  if (deltaZoom >= 0) {
106
108
  this.zoomDistance *= zoomFactor;
@@ -119,19 +121,24 @@ export class SimpleCharacterCameraBehavior {
119
121
  if (collisionOptions === true) {
120
122
  collisionOptions = {};
121
123
  }
122
- let distance = this.raycast?.(rayHelper, this.zoomDistance);
124
+ let distance = raycast?.(rayHelper, this.zoomDistance);
123
125
  if (distance != null) {
124
126
  this.collisionFreeZoomDistance = distance - (collisionOptions?.offset ?? 0.2);
125
127
  }
126
128
  }
127
129
  // Calculate camera position using spherical coordinates from euler
128
130
  sphericalOffset.set(0, 0, this.collisionFreeZoomDistance);
129
- sphericalOffset.applyEuler(this.getCamera().rotation);
131
+ sphericalOffset.applyEuler(camera.rotation);
130
132
  // Get target position with offset (reuse helper vector)
131
- this.character.getWorldPosition(characterWorldPosition);
132
133
  this.computeCharacterBaseOffset(chracterBaseOffsetHelper, options.characterBaseOffset);
134
+ target.getWorldQuaternion(quaternionHelper);
135
+ chracterBaseOffsetHelper.applyQuaternion(quaternionHelper);
136
+ target.getWorldPosition(characterWorldPosition);
133
137
  characterWorldPosition.add(chracterBaseOffsetHelper);
134
138
  // Set camera position relative to target
135
- this.getCamera().position.copy(characterWorldPosition).add(sphericalOffset);
139
+ camera.position.copy(characterWorldPosition).add(sphericalOffset);
140
+ }
141
+ dispose() {
142
+ this.abortController.abort();
136
143
  }
137
144
  }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
- export { extractProxy, getIsMobileMediaQuery, isMobile } from './utils.js';
1
+ export * from './utils.js';
2
2
  export * from './input/index.js';
3
+ export * from './utils.js';
3
4
  export * from './camera.js';
4
5
  export * from './physics/index.js';
5
6
  export * from './animation/index.js';
6
7
  export * from './material.js';
7
- export * from './simple-character.js';
8
+ export * from './simple-character/index.js';
8
9
  export * from './model/index.js';
package/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
- export { extractProxy, getIsMobileMediaQuery, isMobile } from './utils.js';
1
+ export * from './utils.js';
2
2
  export * from './input/index.js';
3
+ export * from './utils.js';
3
4
  export * from './camera.js';
4
5
  export * from './physics/index.js';
5
6
  export * from './animation/index.js';
6
7
  export * from './material.js';
7
- export * from './simple-character.js';
8
+ export * from './simple-character/index.js';
8
9
  export * from './model/index.js';
9
10
  (function injectMobileClassStyle() {
10
11
  if (typeof document === 'undefined') {
@@ -0,0 +1,41 @@
1
+ export declare class EventAction<T = void> {
2
+ private latestTime;
3
+ private readonly subscriptionListeners;
4
+ emit(value: T): void;
5
+ subscribe(callback: (value: T) => void, options?: {
6
+ once?: true;
7
+ signal?: AbortSignal;
8
+ }): void;
9
+ waitFor(signal?: AbortSignal): Promise<T>;
10
+ getLatestTime(): number;
11
+ }
12
+ export type StateActionWriter<T> = {
13
+ write(value: T): void;
14
+ };
15
+ /**
16
+ * StateAction keeps the latest state per writer and merges them on read.
17
+ * Values persist until the writer is disposed (abortSignal aborts).
18
+ */
19
+ export declare class StateAction<T> {
20
+ private readonly mergeWriters;
21
+ private readonly neutral;
22
+ private readonly absoluteActions;
23
+ constructor(mergeWriters: (...values: Array<T>) => T, neutral: T);
24
+ createWriter(abortSignal: AbortSignal): StateActionWriter<T>;
25
+ get(): T;
26
+ }
27
+ /**
28
+ * DeltaAction accumulates transient values each frame and clears them on frame advance.
29
+ * Multiple writes per frame from any source are combined using combine().
30
+ */
31
+ export declare class DeltaAction<T> {
32
+ private readonly combine;
33
+ private readonly neutral;
34
+ constructor(combine: (...values: Array<T>) => T, neutral: T);
35
+ private readonly readers;
36
+ write(value: T): void;
37
+ createReader(abortSignal: AbortSignal): {
38
+ update(): void;
39
+ get(): T;
40
+ };
41
+ }