@pmndrs/viverse 0.1.19 → 0.2.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.
Files changed (57) hide show
  1. package/dist/animation/bvh-bone-map.json +24 -0
  2. package/dist/animation/bvh.d.ts +4 -0
  3. package/dist/animation/bvh.js +8 -0
  4. package/dist/animation/default.d.ts +1 -0
  5. package/dist/animation/default.js +18 -0
  6. package/dist/animation/fbx.d.ts +2 -2
  7. package/dist/animation/gltf.d.ts +2 -2
  8. package/dist/animation/index.d.ts +16 -17
  9. package/dist/animation/index.js +118 -67
  10. package/dist/animation/mask.d.ts +3 -0
  11. package/dist/animation/mask.js +3 -0
  12. package/dist/camera.d.ts +3 -7
  13. package/dist/camera.js +16 -24
  14. package/dist/index.d.ts +3 -2
  15. package/dist/index.js +3 -2
  16. package/dist/input/index.d.ts +6 -7
  17. package/dist/input/index.js +5 -4
  18. package/dist/input/keyboard.d.ts +3 -4
  19. package/dist/input/keyboard.js +8 -10
  20. package/dist/input/pointer-capture.d.ts +3 -3
  21. package/dist/input/pointer-capture.js +11 -12
  22. package/dist/input/pointer-lock.d.ts +3 -3
  23. package/dist/input/pointer-lock.js +10 -10
  24. package/dist/input/screen-joystick.d.ts +6 -10
  25. package/dist/input/screen-joystick.js +29 -36
  26. package/dist/input/screen-jump-button.d.ts +1 -1
  27. package/dist/model/index.d.ts +10 -13
  28. package/dist/model/index.js +26 -29
  29. package/dist/physics/index.d.ts +2 -5
  30. package/dist/physics/index.js +7 -16
  31. package/dist/simple-character/defaults.d.ts +2 -0
  32. package/dist/simple-character/defaults.js +2 -0
  33. package/dist/simple-character/index.d.ts +101 -0
  34. package/dist/simple-character/index.js +109 -0
  35. package/dist/simple-character/state/index.d.ts +6 -0
  36. package/dist/simple-character/state/index.js +6 -0
  37. package/dist/simple-character/state/jump-down.d.ts +3 -0
  38. package/dist/simple-character/state/jump-down.js +25 -0
  39. package/dist/simple-character/state/jump-forward.d.ts +5 -0
  40. package/dist/simple-character/state/jump-forward.js +39 -0
  41. package/dist/simple-character/state/jump-loop.d.ts +3 -0
  42. package/dist/simple-character/state/jump-loop.js +23 -0
  43. package/dist/simple-character/state/jump-start.d.ts +4 -0
  44. package/dist/simple-character/state/jump-start.js +30 -0
  45. package/dist/simple-character/state/jump-up.d.ts +5 -0
  46. package/dist/simple-character/state/jump-up.js +38 -0
  47. package/dist/simple-character/state/movement.d.ts +3 -0
  48. package/dist/simple-character/state/movement.js +59 -0
  49. package/dist/simple-character/update-input-velocity.d.ts +5 -0
  50. package/dist/simple-character/update-input-velocity.js +25 -0
  51. package/dist/simple-character/update-rotation.d.ts +6 -0
  52. package/dist/simple-character/update-rotation.js +40 -0
  53. package/dist/utils.d.ts +12 -5
  54. package/dist/utils.js +28 -39
  55. package/package.json +2 -2
  56. package/dist/simple-character.d.ts +0 -106
  57. package/dist/simple-character.js +0 -342
@@ -0,0 +1,18 @@
1
+ export async function loadDefaultCharacterAnimationUrl(type) {
2
+ switch (type) {
3
+ case 'idle':
4
+ return (await import('../assets/idle.js')).url;
5
+ case 'jumpDown':
6
+ return (await import('../assets/jump-down.js')).url;
7
+ case 'jumpForward':
8
+ return (await import('../assets/jump-forward.js')).url;
9
+ case 'jumpLoop':
10
+ return (await import('../assets/jump-loop.js')).url;
11
+ case 'jumpUp':
12
+ return (await import('../assets/jump-up.js')).url;
13
+ case 'run':
14
+ return (await import('../assets/run.js')).url;
15
+ case 'walk':
16
+ return (await import('../assets/walk.js')).url;
17
+ }
18
+ }
@@ -1,4 +1,4 @@
1
1
  import { AnimationClip } from 'three';
2
- import { loadCharacterModel } from '../model/index.js';
2
+ import type { CharacterModel } from '../model/index.js';
3
3
  import type { VRMHumanBoneName } from '@pixiv/three-vrm';
4
- export declare function loadVrmModelFbxAnimations(model: Exclude<Awaited<ReturnType<typeof loadCharacterModel>>, undefined>, url: string, removeXZMovement: boolean, boneMap?: Record<string, VRMHumanBoneName>): Promise<Array<AnimationClip>>;
4
+ export declare function loadVrmModelFbxAnimations(model: CharacterModel, url: string, removeXZMovement: boolean, boneMap?: Record<string, VRMHumanBoneName>): Promise<Array<AnimationClip>>;
@@ -1,3 +1,3 @@
1
1
  import { AnimationClip } from 'three';
2
- import { loadCharacterModel, VRMHumanBoneName } from '../model/index.js';
3
- export declare function loadVrmModelGltfAnimations(model: Exclude<Awaited<ReturnType<typeof loadCharacterModel>>, undefined>, url: string, removeXZMovement: boolean, boneMap?: Record<string, VRMHumanBoneName>): Promise<Array<AnimationClip>>;
2
+ import type { CharacterModel, VRMHumanBoneName } from '../model/index.js';
3
+ export declare function loadVrmModelGltfAnimations(model: CharacterModel, url: string, removeXZMovement: boolean, boneMap?: Record<string, VRMHumanBoneName>): Promise<Array<AnimationClip>>;
@@ -1,14 +1,18 @@
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, removeXZMovement: boolean, boneMap?: Record<string, VRMHumanBoneName>): void;
3
+ import { loadDefaultCharacterAnimationUrl } from './default.js';
4
+ import type { CharacterModel } from '../model/index.js';
5
+ export declare function fixModelAnimationClip(model: CharacterModel, clip: AnimationClip, clipScene: Object3D | undefined, removeXZMovement: boolean, boneMap?: Record<string, VRMHumanBoneName>): void;
5
6
  export * from './gltf.js';
6
7
  export * from './fbx.js';
7
8
  export * from './vrma.js';
8
9
  export * from './utils.js';
9
- export type ModelAnimationOptions = {
10
- type: 'mixamo' | 'gltf' | 'vrma' | 'fbx';
11
- url: string;
10
+ export type CharacterAnimationMask = (boneName: VRMHumanBoneName) => boolean;
11
+ export type CharacterAnimationOptions = {
12
+ url: string | {
13
+ default: Parameters<typeof loadDefaultCharacterAnimationUrl>[0];
14
+ };
15
+ type?: 'mixamo' | 'gltf' | 'vrma' | 'fbx' | 'bvh';
12
16
  removeXZMovement?: boolean;
13
17
  trimTime?: {
14
18
  start?: number;
@@ -16,17 +20,12 @@ export type ModelAnimationOptions = {
16
20
  };
17
21
  boneMap?: Record<string, VRMHumanBoneName>;
18
22
  scaleTime?: number;
23
+ mask?: CharacterAnimationMask;
19
24
  };
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>;
25
+ export type Tail<T extends any[]> = T extends [any, ...infer Rest] ? Rest : never;
26
+ export declare function flattenCharacterAnimationOptions(options: Exclude<CharacterAnimationOptions, false>): Tail<Parameters<typeof loadCharacterAnimation>>;
27
+ export declare function loadCharacterAnimation(model: CharacterModel, url: string | {
28
+ default: Parameters<typeof loadDefaultCharacterAnimationUrl>[0];
29
+ }, type?: CharacterAnimationOptions['type'], removeXZMovement?: boolean, trimStartTime?: number | undefined, trimEndTime?: number | undefined, boneMap?: Record<string, VRMHumanBoneName> | undefined, scaleTime?: number | undefined, mask?: CharacterAnimationMask): Promise<AnimationClip>;
32
30
  export declare const mixamoBoneMap: Record<string, VRMHumanBoneName>;
31
+ 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';
5
+ import { loadDefaultCharacterAnimationUrl } from './default.js';
3
6
  import { loadVrmModelFbxAnimations } from './fbx.js';
4
7
  import { loadVrmModelGltfAnimations } from './gltf.js';
8
+ import _mixamoBoneMap from './mixamo-bone-map.json';
5
9
  import { scaleAnimationClipTime, trimAnimationClip } from './utils.js';
6
10
  import { loadVrmModelVrmaAnimations } from './vrma.js';
7
- 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) {
61
+ continue;
62
+ }
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) {
47
81
  continue;
48
82
  }
49
- const vrmNodeName = model instanceof VRM ? targetBone.name : vrmBoneName;
50
- const baseBone = clipScene.getObjectByName(boneName);
51
- if (vrmNodeName == null || baseBone == null) {
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,15 +163,53 @@ 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';
128
175
  export * from './vrma.js';
129
176
  export * from './utils.js';
130
- async function uncachedLoadModelAnimation(model, type, url, removeXZMovement, trimStartTime, trimEndTime, boneMap, scaleTime) {
177
+ export function flattenCharacterAnimationOptions(options) {
178
+ return [
179
+ options.url,
180
+ options.type,
181
+ options.removeXZMovement,
182
+ options.trimTime?.start,
183
+ options.trimTime?.end,
184
+ options.boneMap,
185
+ options.scaleTime,
186
+ options.mask,
187
+ ];
188
+ }
189
+ export async function loadCharacterAnimation(model, url, type, removeXZMovement = false, trimStartTime, trimEndTime, boneMap, scaleTime, mask) {
190
+ if (typeof url === 'object') {
191
+ url = await loadDefaultCharacterAnimationUrl(url.default);
192
+ type = 'gltf';
193
+ }
131
194
  let clips;
195
+ if (type == null) {
196
+ const lowerCaseUrl = url.toLocaleLowerCase();
197
+ if (lowerCaseUrl.endsWith('.glb') || lowerCaseUrl.endsWith('.gltf')) {
198
+ type = 'gltf';
199
+ }
200
+ if (lowerCaseUrl.endsWith('.fbx')) {
201
+ type = 'fbx';
202
+ }
203
+ if (lowerCaseUrl.endsWith('.bvh')) {
204
+ type = 'bvh';
205
+ }
206
+ if (lowerCaseUrl.endsWith('.vrma')) {
207
+ type = 'vrma';
208
+ }
209
+ if (type == null) {
210
+ throw new Error(`Unable to infer animation type from url "${url}. Please specify the type of the animation manually."`);
211
+ }
212
+ }
132
213
  switch (type) {
133
214
  case 'gltf':
134
215
  clips = await loadVrmModelGltfAnimations(model, url, removeXZMovement, boneMap);
@@ -136,6 +217,9 @@ async function uncachedLoadModelAnimation(model, type, url, removeXZMovement, tr
136
217
  case 'fbx':
137
218
  clips = await loadVrmModelFbxAnimations(model, url, removeXZMovement, boneMap);
138
219
  break;
220
+ case 'bvh':
221
+ clips = await loadVrmModelBvhAnimations(model, url, removeXZMovement, boneMap ?? bvhBoneMap);
222
+ break;
139
223
  case 'mixamo':
140
224
  clips = await loadVrmModelFbxAnimations(model, url, removeXZMovement, boneMap ?? mixamoBoneMap);
141
225
  break;
@@ -158,38 +242,5 @@ async function uncachedLoadModelAnimation(model, type, url, removeXZMovement, tr
158
242
  }
159
243
  return clip;
160
244
  }
161
- export function loadCharacterModelAnimation(model, options) {
162
- return cached(uncachedLoadModelAnimation, [
163
- model,
164
- options.type,
165
- options.url,
166
- options.removeXZMovement ?? false,
167
- options.trimTime?.start,
168
- options.trimTime?.end,
169
- options.boneMap,
170
- options.scaleTime,
171
- ]);
172
- }
173
- const extraOptions = {
174
- walk: { scaleTime: 0.5 },
175
- run: { scaleTime: 0.8 },
176
- jumpForward: { scaleTime: 0.9 },
177
- };
178
- const simpleCharacterAnimationUrls = {
179
- walk: () => import('../assets/walk.js'),
180
- run: () => import('../assets/run.js'),
181
- idle: () => import('../assets/idle.js'),
182
- jumpUp: () => import('../assets/jump-up.js'),
183
- jumpLoop: () => import('../assets/jump-loop.js'),
184
- jumpDown: () => import('../assets/jump-down.js'),
185
- jumpForward: () => import('../assets/jump-forward.js'),
186
- };
187
- export const simpleCharacterAnimationNames = Object.keys(simpleCharacterAnimationUrls);
188
- export async function getSimpleCharacterModelAnimationOptions(animationName) {
189
- return {
190
- type: 'gltf',
191
- ...extraOptions[animationName],
192
- url: (await simpleCharacterAnimationUrls[animationName]()).url,
193
- };
194
- }
195
245
  export const mixamoBoneMap = _mixamoBoneMap;
246
+ export const bvhBoneMap = _bvhBoneMap;
@@ -0,0 +1,3 @@
1
+ import type { CharacterAnimationMask } from './index.js';
2
+ import type { AnimationClip } from 'three';
3
+ export declare function applyMask(clip: AnimationClip, mask: CharacterAnimationMask): void;
@@ -0,0 +1,3 @@
1
+ export function applyMask(clip, mask) {
2
+ clip.tracks = clip.tracks.filter((track) => mask(track.name.split('.')[0]));
3
+ }
package/dist/camera.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Object3D, Vector3, Vector3Tuple, Ray } from 'three';
2
- import { SimpleCharacter } from './simple-character.js';
2
+ import { InputSystem } from './input/index.js';
3
3
  export declare const FirstPersonCharacterCameraBehavior: SimpleCharacterCameraBehaviorOptions;
4
4
  export type SimpleCharacterCameraBehaviorOptions = {
5
5
  /**
@@ -55,16 +55,12 @@ export type SimpleCharacterCameraBehaviorOptions = {
55
55
  maxDistance?: number;
56
56
  } | boolean;
57
57
  } | boolean;
58
- export declare class SimpleCharacterCameraBehavior {
59
- getCamera: () => Object3D;
60
- character: SimpleCharacter;
61
- private readonly raycast?;
58
+ export declare class CharacterCameraBehavior {
62
59
  rotationPitch: number;
63
60
  rotationYaw: number;
64
61
  zoomDistance: number;
65
62
  private collisionFreeZoomDistance;
66
63
  private firstUpdate;
67
- constructor(getCamera: () => Object3D, character: SimpleCharacter, raycast?: ((ray: Ray, far: number) => number | undefined) | undefined);
68
64
  private setRotationFromDelta;
69
65
  private setDistanceFromDelta;
70
66
  private computeCharacterBaseOffset;
@@ -74,5 +70,5 @@ export declare class SimpleCharacterCameraBehavior {
74
70
  /**
75
71
  * @param delta in seconds
76
72
  */
77
- update(deltaTime: number, options?: SimpleCharacterCameraBehaviorOptions): void;
73
+ update(camera: Object3D, target: Object3D, inputSystem: InputSystem, deltaTime: number, raycast?: (ray: Ray, far: number) => number | undefined, options?: SimpleCharacterCameraBehaviorOptions): void;
78
74
  }
package/dist/camera.js CHANGED
@@ -11,25 +11,17 @@ 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
+ export class CharacterCameraBehavior {
18
15
  rotationPitch = (-20 * Math.PI) / 180;
19
16
  rotationYaw = 0;
20
17
  zoomDistance = 4; // Changed from zoom to distance for clearer semantics
21
18
  //internal state
22
19
  collisionFreeZoomDistance = this.zoomDistance;
23
20
  firstUpdate = true;
24
- constructor(getCamera, character, raycast) {
25
- this.getCamera = getCamera;
26
- this.character = character;
27
- this.raycast = raycast;
28
- }
29
- setRotationFromDelta(delta, rotationOptions) {
21
+ setRotationFromDelta(camera, delta, rotationOptions) {
30
22
  if (delta.lengthSq() < 0.0001) {
31
23
  // use current camera rotation if very close to target
32
- euler.setFromQuaternion(this.getCamera().quaternion, 'YXZ');
24
+ euler.setFromQuaternion(camera.quaternion, 'YXZ');
33
25
  this.rotationPitch = euler.x;
34
26
  this.rotationYaw = euler.y;
35
27
  return;
@@ -63,7 +55,7 @@ export class SimpleCharacterCameraBehavior {
63
55
  /**
64
56
  * @param delta in seconds
65
57
  */
66
- update(deltaTime, options = true) {
58
+ update(camera, target, inputSystem, deltaTime, raycast, options = true) {
67
59
  if (options === false) {
68
60
  this.firstUpdate = true;
69
61
  return;
@@ -74,33 +66,33 @@ export class SimpleCharacterCameraBehavior {
74
66
  }
75
67
  //compute character->camera delta through offset
76
68
  this.computeCharacterBaseOffset(chracterBaseOffsetHelper, options.characterBaseOffset);
77
- this.character.getWorldPosition(characterWorldPosition);
69
+ target.getWorldPosition(characterWorldPosition);
78
70
  characterWorldPosition.add(chracterBaseOffsetHelper);
79
- this.getCamera().getWorldPosition(deltaHelper);
71
+ camera.getWorldPosition(deltaHelper);
80
72
  deltaHelper.sub(characterWorldPosition);
81
73
  // apply rotation input to rotationYaw and rotationPitch if not disabled or first update
82
74
  let rotationOptions = options.rotation ?? true;
83
75
  if (!this.firstUpdate && rotationOptions !== false) {
84
76
  rotationOptions = rotationOptions === true ? {} : rotationOptions;
85
77
  const rotationSpeed = rotationOptions.speed ?? 1000.0;
86
- const deltaYaw = this.character.inputSystem.get(DeltaYawField);
87
- const deltaPitch = this.character.inputSystem.get(DeltaPitchField);
78
+ const deltaYaw = inputSystem.get(DeltaYawField);
79
+ const deltaPitch = inputSystem.get(DeltaPitchField);
88
80
  this.rotationYaw = this.clampYaw(this.rotationYaw + deltaYaw * rotationSpeed * deltaTime, rotationOptions);
89
81
  this.rotationPitch = this.clampPitch(this.rotationPitch + deltaPitch * rotationSpeed * deltaTime, rotationOptions);
90
82
  }
91
83
  else {
92
- this.setRotationFromDelta(deltaHelper, typeof rotationOptions === 'boolean' ? {} : rotationOptions);
84
+ this.setRotationFromDelta(camera, deltaHelper, typeof rotationOptions === 'boolean' ? {} : rotationOptions);
93
85
  }
94
86
  // 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);
87
+ camera.rotation.set(this.rotationPitch, this.rotationYaw, 0, 'YXZ');
88
+ rayHelper.direction.set(0, 0, 1).applyEuler(camera.rotation);
97
89
  rayHelper.origin.copy(characterWorldPosition);
98
90
  // apply zoom input to zoomDistance if not disabled or first update
99
91
  let zoomOptions = options.zoom ?? true;
100
92
  if (!this.firstUpdate && zoomOptions !== false) {
101
93
  zoomOptions = zoomOptions === true ? {} : zoomOptions;
102
94
  const zoomSpeed = zoomOptions.speed ?? 1000.0;
103
- const deltaZoom = this.character.inputSystem.get(DeltaZoomField);
95
+ const deltaZoom = inputSystem.get(DeltaZoomField);
104
96
  const zoomFactor = 1 + deltaZoom * zoomSpeed * deltaTime;
105
97
  if (deltaZoom >= 0) {
106
98
  this.zoomDistance *= zoomFactor;
@@ -119,19 +111,19 @@ export class SimpleCharacterCameraBehavior {
119
111
  if (collisionOptions === true) {
120
112
  collisionOptions = {};
121
113
  }
122
- let distance = this.raycast?.(rayHelper, this.zoomDistance);
114
+ let distance = raycast?.(rayHelper, this.zoomDistance);
123
115
  if (distance != null) {
124
116
  this.collisionFreeZoomDistance = distance - (collisionOptions?.offset ?? 0.2);
125
117
  }
126
118
  }
127
119
  // Calculate camera position using spherical coordinates from euler
128
120
  sphericalOffset.set(0, 0, this.collisionFreeZoomDistance);
129
- sphericalOffset.applyEuler(this.getCamera().rotation);
121
+ sphericalOffset.applyEuler(camera.rotation);
130
122
  // Get target position with offset (reuse helper vector)
131
- this.character.getWorldPosition(characterWorldPosition);
123
+ target.getWorldPosition(characterWorldPosition);
132
124
  this.computeCharacterBaseOffset(chracterBaseOffsetHelper, options.characterBaseOffset);
133
125
  characterWorldPosition.add(chracterBaseOffsetHelper);
134
126
  // Set camera position relative to target
135
- this.getCamera().position.copy(characterWorldPosition).add(sphericalOffset);
127
+ camera.position.copy(characterWorldPosition).add(sphericalOffset);
136
128
  }
137
129
  }
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') {
@@ -1,8 +1,7 @@
1
- export declare class InputSystem {
2
- private readonly inputs;
3
- constructor(domElement: HTMLElement, inputs: ReadonlyArray<Input | {
4
- new (element: HTMLElement, options?: {}): Input;
5
- }>, options?: {});
1
+ export declare class InputSystem<T extends {} = {}> {
2
+ options: T;
3
+ readonly inputs: Array<Input>;
4
+ constructor(options?: T);
6
5
  add(input: Input): void;
7
6
  remove(input: Input): void;
8
7
  dispose(): void;
@@ -21,8 +20,8 @@ export declare const RunField: InputField<boolean>;
21
20
  export declare const DeltaZoomField: InputField<number>;
22
21
  export declare const DeltaYawField: InputField<number>;
23
22
  export declare const DeltaPitchField: InputField<number>;
24
- export interface Input {
25
- get<T>(field: InputField<T>): T | undefined;
23
+ export interface Input<O = {}> {
24
+ get<T>(field: InputField<T>, options: O): T | undefined;
26
25
  dispose?(): void;
27
26
  }
28
27
  export * from './pointer-lock.js';
@@ -1,7 +1,8 @@
1
1
  export class InputSystem {
2
- inputs;
3
- constructor(domElement, inputs, options) {
4
- this.inputs = inputs.map((input) => (typeof input === 'function' ? new input(domElement, options) : input));
2
+ options;
3
+ inputs = [];
4
+ constructor(options = {}) {
5
+ this.options = options;
5
6
  }
6
7
  add(input) {
7
8
  this.inputs.push(input);
@@ -20,7 +21,7 @@ export class InputSystem {
20
21
  get(field) {
21
22
  let current;
22
23
  for (const input of this.inputs) {
23
- const result = input.get(field);
24
+ const result = input.get(field, this.options);
24
25
  if (result == null) {
25
26
  continue;
26
27
  }
@@ -7,11 +7,10 @@ export type LocomotionKeyboardInputOptions = {
7
7
  keyboardRunKeys?: Array<string>;
8
8
  keyboardJumpKeys?: Array<string>;
9
9
  };
10
- export declare class LocomotionKeyboardInput implements Input {
11
- private readonly options;
10
+ export declare class LocomotionKeyboardInput implements Input<LocomotionKeyboardInputOptions> {
12
11
  private readonly abortController;
13
12
  private readonly keyState;
14
- constructor(domElement: HTMLElement, options?: LocomotionKeyboardInputOptions);
15
- get<T>(field: InputField<T>): T | undefined;
13
+ constructor(domElement: HTMLElement);
14
+ get<T>(field: InputField<T>, options: LocomotionKeyboardInputOptions): T | undefined;
16
15
  dispose(): void;
17
16
  }
@@ -6,11 +6,9 @@ const DefaultMoveRightKeys = ['KeyD'];
6
6
  const DefaultRunKeys = ['ShiftRight', 'ShiftLeft'];
7
7
  const DefaultJumpKeys = ['Space'];
8
8
  export class LocomotionKeyboardInput {
9
- options;
10
9
  abortController = new AbortController();
11
10
  keyState = new Map();
12
- constructor(domElement, options = {}) {
13
- this.options = options;
11
+ constructor(domElement) {
14
12
  domElement.tabIndex = 0;
15
13
  domElement.addEventListener('keydown', (event) => {
16
14
  let state = this.keyState.get(event.code);
@@ -41,9 +39,9 @@ export class LocomotionKeyboardInput {
41
39
  signal: this.abortController.signal,
42
40
  });
43
41
  }
44
- get(field) {
42
+ get(field, options) {
45
43
  if (field === LastTimeJumpPressedField) {
46
- const jumpKeys = this.options.keyboardJumpKeys ?? DefaultJumpKeys;
44
+ const jumpKeys = options.keyboardJumpKeys ?? DefaultJumpKeys;
47
45
  const pressed = jumpKeys
48
46
  .map((key) => this.keyState.get(key)?.pressTime ?? null)
49
47
  .filter((t) => t != null);
@@ -52,19 +50,19 @@ export class LocomotionKeyboardInput {
52
50
  let keys;
53
51
  switch (field) {
54
52
  case MoveForwardField:
55
- keys = this.options.keyboardMoveForwardKeys ?? DefaultMoveForwardKeys;
53
+ keys = options.keyboardMoveForwardKeys ?? DefaultMoveForwardKeys;
56
54
  break;
57
55
  case MoveBackwardField:
58
- keys = this.options.keyboardMoveBackwardKeys ?? DefaultMoveBackwardKeys;
56
+ keys = options.keyboardMoveBackwardKeys ?? DefaultMoveBackwardKeys;
59
57
  break;
60
58
  case MoveLeftField:
61
- keys = this.options.keyboardMoveLeftKeys ?? DefaultMoveLeftKeys;
59
+ keys = options.keyboardMoveLeftKeys ?? DefaultMoveLeftKeys;
62
60
  break;
63
61
  case MoveRightField:
64
- keys = this.options.keyboardMoveRightKeys ?? DefaultMoveRightKeys;
62
+ keys = options.keyboardMoveRightKeys ?? DefaultMoveRightKeys;
65
63
  break;
66
64
  case RunField:
67
- keys = this.options.keyboardRunKeys ?? DefaultRunKeys;
65
+ keys = options.keyboardRunKeys ?? DefaultRunKeys;
68
66
  break;
69
67
  }
70
68
  if (keys == null) {