@pmndrs/viverse 0.1.20 → 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 (55) hide show
  1. package/dist/animation/bvh.d.ts +2 -2
  2. package/dist/animation/default.d.ts +1 -0
  3. package/dist/animation/default.js +18 -0
  4. package/dist/animation/fbx.d.ts +2 -2
  5. package/dist/animation/gltf.d.ts +2 -2
  6. package/dist/animation/index.d.ts +14 -16
  7. package/dist/animation/index.js +18 -36
  8. package/dist/animation/mask.d.ts +3 -0
  9. package/dist/animation/mask.js +3 -0
  10. package/dist/camera.d.ts +3 -7
  11. package/dist/camera.js +16 -24
  12. package/dist/index.d.ts +3 -2
  13. package/dist/index.js +3 -2
  14. package/dist/input/index.d.ts +6 -7
  15. package/dist/input/index.js +5 -4
  16. package/dist/input/keyboard.d.ts +3 -4
  17. package/dist/input/keyboard.js +8 -10
  18. package/dist/input/pointer-capture.d.ts +3 -3
  19. package/dist/input/pointer-capture.js +11 -12
  20. package/dist/input/pointer-lock.d.ts +3 -3
  21. package/dist/input/pointer-lock.js +10 -10
  22. package/dist/input/screen-joystick.d.ts +6 -10
  23. package/dist/input/screen-joystick.js +29 -36
  24. package/dist/input/screen-jump-button.d.ts +1 -1
  25. package/dist/model/index.d.ts +10 -13
  26. package/dist/model/index.js +12 -34
  27. package/dist/physics/index.d.ts +2 -5
  28. package/dist/physics/index.js +7 -16
  29. package/dist/simple-character/defaults.d.ts +2 -0
  30. package/dist/simple-character/defaults.js +2 -0
  31. package/dist/simple-character/index.d.ts +101 -0
  32. package/dist/simple-character/index.js +109 -0
  33. package/dist/simple-character/state/index.d.ts +6 -0
  34. package/dist/simple-character/state/index.js +6 -0
  35. package/dist/simple-character/state/jump-down.d.ts +3 -0
  36. package/dist/simple-character/state/jump-down.js +25 -0
  37. package/dist/simple-character/state/jump-forward.d.ts +5 -0
  38. package/dist/simple-character/state/jump-forward.js +39 -0
  39. package/dist/simple-character/state/jump-loop.d.ts +3 -0
  40. package/dist/simple-character/state/jump-loop.js +23 -0
  41. package/dist/simple-character/state/jump-start.d.ts +4 -0
  42. package/dist/simple-character/state/jump-start.js +30 -0
  43. package/dist/simple-character/state/jump-up.d.ts +5 -0
  44. package/dist/simple-character/state/jump-up.js +38 -0
  45. package/dist/simple-character/state/movement.d.ts +3 -0
  46. package/dist/simple-character/state/movement.js +59 -0
  47. package/dist/simple-character/update-input-velocity.d.ts +5 -0
  48. package/dist/simple-character/update-input-velocity.js +25 -0
  49. package/dist/simple-character/update-rotation.d.ts +6 -0
  50. package/dist/simple-character/update-rotation.js +40 -0
  51. package/dist/utils.d.ts +12 -5
  52. package/dist/utils.js +28 -39
  53. package/package.json +2 -2
  54. package/dist/simple-character.d.ts +0 -107
  55. package/dist/simple-character.js +0 -344
@@ -1,19 +1,15 @@
1
1
  import { MoveForwardField, MoveBackwardField, MoveLeftField, MoveRightField, RunField, } from './index.js';
2
2
  const DefaultDeadZonePx = 24;
3
3
  const DefaultRunDistancePx = 46;
4
+ const JoystickRadius = 56;
4
5
  export class ScreenJoystickInput {
5
- options;
6
6
  root;
7
7
  handle;
8
- moveX = 0;
9
- moveY = 0;
10
- running = false;
11
- joystickRadius = 56;
12
- joyCenterX = 0;
13
- joyCenterY = 0;
14
8
  pointerId;
15
- constructor(domElement, options = {}) {
16
- this.options = options;
9
+ distanceToCenter = 0;
10
+ clampedX = 0;
11
+ clampedY = 0;
12
+ constructor(domElement) {
17
13
  const parent = domElement.parentElement ?? domElement;
18
14
  const joy = document.createElement('div');
19
15
  joy.className = 'viverse-joystick mobile-only';
@@ -58,9 +54,9 @@ export class ScreenJoystickInput {
58
54
  joy.setPointerCapture(e.pointerId);
59
55
  this.pointerId = e.pointerId;
60
56
  const rect = joy.getBoundingClientRect();
61
- this.joyCenterX = rect.left + rect.width / 2;
62
- this.joyCenterY = rect.top + rect.height / 2;
63
- this.updateHandle(e.clientX - this.joyCenterX, e.clientY - this.joyCenterY);
57
+ const joyCenterX = rect.left + rect.width / 2;
58
+ const joyCenterY = rect.top + rect.height / 2;
59
+ this.updateHandle(e.clientX - joyCenterX, e.clientY - joyCenterY);
64
60
  };
65
61
  const onPointerMove = (e) => {
66
62
  if (this.pointerId == null) {
@@ -68,7 +64,10 @@ export class ScreenJoystickInput {
68
64
  }
69
65
  e.preventDefault();
70
66
  e.stopPropagation();
71
- this.updateHandle(e.clientX - this.joyCenterX, e.clientY - this.joyCenterY);
67
+ const rect = joy.getBoundingClientRect();
68
+ const joyCenterX = rect.left + rect.width / 2;
69
+ const joyCenterY = rect.top + rect.height / 2;
70
+ this.updateHandle(e.clientX - joyCenterX, e.clientY - joyCenterY);
72
71
  };
73
72
  const onPointerEnd = (e) => {
74
73
  if (this.pointerId != e.pointerId) {
@@ -84,18 +83,22 @@ export class ScreenJoystickInput {
84
83
  joy.addEventListener('pointerup', onPointerEnd);
85
84
  joy.addEventListener('pointercancel', onPointerEnd);
86
85
  }
87
- get(field) {
86
+ get(field, options) {
88
87
  switch (field) {
89
88
  case MoveForwardField:
90
- return Math.max(0, this.moveY);
91
89
  case MoveBackwardField:
92
- return Math.max(0, -this.moveY);
90
+ const moveY = this.distanceToCenter <= (options.screenJoystickDeadZonePx ?? DefaultDeadZonePx)
91
+ ? 0
92
+ : -this.clampedY / JoystickRadius;
93
+ return field === MoveForwardField ? Math.max(0, moveY) : Math.max(0, -moveY);
93
94
  case MoveLeftField:
94
- return Math.max(0, -this.moveX);
95
95
  case MoveRightField:
96
- return Math.max(0, this.moveX);
96
+ const moveX = this.distanceToCenter <= (options.screenJoystickDeadZonePx ?? DefaultDeadZonePx)
97
+ ? 0
98
+ : this.clampedX / JoystickRadius;
99
+ return field === MoveLeftField ? Math.max(0, moveX) : Math.max(0, moveX);
97
100
  case RunField:
98
- return this.running;
101
+ return (this.distanceToCenter > (options.screenJoystickRunDistancePx ?? DefaultRunDistancePx));
99
102
  }
100
103
  return undefined;
101
104
  }
@@ -103,25 +106,15 @@ export class ScreenJoystickInput {
103
106
  this.root.remove();
104
107
  }
105
108
  updateHandle(dx, dy) {
106
- const len = Math.hypot(dx, dy) || 1;
107
- const max = this.joystickRadius;
108
- const clampedX = (dx / len) * Math.min(len, max);
109
- const clampedY = (dy / len) * Math.min(len, max);
110
- this.handle.style.transform = `translate(-50%,-50%) translate(${clampedX}px, ${clampedY}px)`;
111
- if (len <= (this.options.screenJoystickDeadZonePx ?? DefaultDeadZonePx)) {
112
- this.moveX = 0;
113
- this.moveY = 0;
114
- }
115
- else {
116
- this.moveX = clampedX / max;
117
- this.moveY = -clampedY / max;
118
- }
119
- this.running = len > (this.options.screenJoystickRunDistancePx ?? DefaultRunDistancePx);
109
+ this.distanceToCenter = Math.hypot(dx, dy) || 1;
110
+ this.clampedX = (dx / this.distanceToCenter) * Math.min(this.distanceToCenter, JoystickRadius);
111
+ this.clampedY = (dy / this.distanceToCenter) * Math.min(this.distanceToCenter, JoystickRadius);
112
+ this.handle.style.transform = `translate(-50%,-50%) translate(${this.clampedX}px, ${this.clampedY}px)`;
120
113
  }
121
114
  resetHandle() {
122
115
  this.handle.style.transform = 'translate(-50%,-50%)';
123
- this.moveX = 0;
124
- this.moveY = 0;
125
- this.running = false;
116
+ this.distanceToCenter = 0;
117
+ this.clampedX = 0;
118
+ this.clampedY = 0;
126
119
  }
127
120
  }
@@ -1,5 +1,5 @@
1
1
  import { Input, InputField } from './index.js';
2
- export declare class ScreenJumpButtonInput implements Input {
2
+ export declare class ScreenJumpButtonInput implements Input<{}> {
3
3
  readonly root: HTMLDivElement;
4
4
  private lastJumpTime;
5
5
  constructor(domElement: HTMLElement);
@@ -1,4 +1,5 @@
1
- import { Quaternion } from 'three';
1
+ import { AnimationAction, AnimationMixer, Object3D, Quaternion } from 'three';
2
+ import { CharacterAnimationMask } from '../animation/index.js';
2
3
  export { VRMHumanBoneName } from '@pixiv/three-vrm';
3
4
  export * from './vrm.js';
4
5
  export type CharacterModelOptions = {
@@ -17,16 +18,12 @@ export type CharacterModelOptions = {
17
18
  * @default true
18
19
  */
19
20
  readonly receiveShadow?: boolean;
20
- } | boolean;
21
- export declare function clearCharacterModelCache(options?: CharacterModelOptions): void;
22
- export declare function loadCharacterModel(options?: CharacterModelOptions): Promise<((import("@pixiv/three-vrm").VRM & {
23
- scene: import("three").Object3D<import("three").Object3DEventMap & {
24
- dispose: {};
25
- }>;
26
- }) | (import("three/examples/jsm/Addons.js").GLTF & {
27
- scene: import("three").Object3D<import("three").Object3DEventMap & {
28
- dispose: {};
29
- }>;
30
- })) & {
21
+ };
22
+ export declare function flattenCharacterModelOptions(options: Exclude<CharacterModelOptions, false> | undefined): Parameters<typeof loadCharacterModel>;
23
+ export type CharacterModel = {
24
+ mixer: AnimationMixer;
25
+ scene: Object3D;
26
+ currentAnimations: Map<CharacterAnimationMask | undefined, AnimationAction>;
31
27
  boneRotationOffset?: Quaternion;
32
- }> | undefined;
28
+ };
29
+ export declare function loadCharacterModel(url?: string, type?: Exclude<CharacterModelOptions, boolean>['type'], boneRotationOffset?: Quaternion, castShadow?: boolean, receiveShadow?: boolean): Promise<CharacterModel>;
@@ -1,10 +1,15 @@
1
- import { Euler, Quaternion } from 'three';
2
- import { loadVrmCharacterModel } from './vrm.js';
3
- import { cached, clearCache } from '../utils.js';
1
+ import { AnimationMixer, Euler, Quaternion } from 'three';
4
2
  import { loadGltfCharacterModel } from './gltf.js';
3
+ import { loadVrmCharacterModel } from './vrm.js';
5
4
  export { VRMHumanBoneName } from '@pixiv/three-vrm';
6
5
  export * from './vrm.js';
7
- async function uncachedLoadCharacterModel(type, url, boneRotationOffset, castShadow = true, receiveShadow = true) {
6
+ export function flattenCharacterModelOptions(options) {
7
+ if (options == null) {
8
+ return [];
9
+ }
10
+ return [options.url, options.type, options.boneRotationOffset, options.castShadow, options.receiveShadow];
11
+ }
12
+ export async function loadCharacterModel(url, type, boneRotationOffset, castShadow = true, receiveShadow = true) {
8
13
  let result;
9
14
  if (url == null) {
10
15
  //prepare loading the default model
@@ -41,36 +46,9 @@ async function uncachedLoadCharacterModel(type, url, boneRotationOffset, castSha
41
46
  obj.receiveShadow = true;
42
47
  }
43
48
  });
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
+ const restPose = result.scene.clone();
49
50
  restPose.visible = false;
50
- restPose.traverse((bone) => (bone.name = `rest_${bone.name}`));
51
+ restPose.traverse((object) => (object.name = `rest_${object.name}`));
51
52
  result.scene.add(restPose);
52
- return result;
53
- }
54
- function getCharacterModelDependencies(options = true) {
55
- if (options === false) {
56
- return undefined;
57
- }
58
- if (options === true) {
59
- return [undefined, undefined, undefined, undefined, undefined];
60
- }
61
- return [options.type, options.url, options.boneRotationOffset, options.castShadow, options.receiveShadow];
62
- }
63
- export function clearCharacterModelCache(options) {
64
- const dependencies = getCharacterModelDependencies(options);
65
- if (dependencies == null) {
66
- return;
67
- }
68
- clearCache(uncachedLoadCharacterModel, dependencies);
69
- }
70
- export function loadCharacterModel(options) {
71
- const dependencies = getCharacterModelDependencies(options);
72
- if (dependencies == null) {
73
- return undefined;
74
- }
75
- return cached(uncachedLoadCharacterModel, dependencies);
53
+ return Object.assign(result, { mixer: new AnimationMixer(result.scene), currentAnimations: new Map() });
76
54
  }
@@ -32,9 +32,7 @@ export type BvhCharacterPhysicsOptions = {
32
32
  * assumes the target object origin is at its bottom
33
33
  */
34
34
  export declare class BvhCharacterPhysics {
35
- private readonly character;
36
35
  private readonly world;
37
- private disposed;
38
36
  private readonly stateVelocity;
39
37
  readonly inputVelocity: Vector3;
40
38
  private notGroundedSeconds;
@@ -42,14 +40,13 @@ export declare class BvhCharacterPhysics {
42
40
  private readonly aabbox;
43
41
  private radius;
44
42
  get isGrounded(): boolean;
45
- constructor(character: Object3D, world: BvhPhysicsWorld);
43
+ constructor(world: BvhPhysicsWorld);
46
44
  applyVelocity(velocity: Vector3): void;
47
45
  /**
48
46
  * @param delta in seconds
49
47
  */
50
- update(fullDelta: number, options?: BvhCharacterPhysicsOptions): void;
48
+ update(model: Object3D, fullDelta: number, options?: BvhCharacterPhysicsOptions): void;
51
49
  private updateBoundingShapes;
52
- dispose(): void;
53
50
  shapecastCapsule(position: Vector3, maxGroundSlope: number, options: Exclude<BvhCharacterPhysicsOptions, boolean>): boolean;
54
51
  }
55
52
  export * from './world.js';
@@ -11,9 +11,7 @@ const YAxis = new Vector3(0, 1, 0);
11
11
  * assumes the target object origin is at its bottom
12
12
  */
13
13
  export class BvhCharacterPhysics {
14
- character;
15
14
  world;
16
- disposed = false;
17
15
  stateVelocity = new Vector3();
18
16
  inputVelocity = new Vector3();
19
17
  notGroundedSeconds = 0;
@@ -23,8 +21,7 @@ export class BvhCharacterPhysics {
23
21
  get isGrounded() {
24
22
  return this.notGroundedSeconds < 0.2;
25
23
  }
26
- constructor(character, world) {
27
- this.character = character;
24
+ constructor(world) {
28
25
  this.world = world;
29
26
  }
30
27
  applyVelocity(velocity) {
@@ -33,16 +30,13 @@ export class BvhCharacterPhysics {
33
30
  /**
34
31
  * @param delta in seconds
35
32
  */
36
- update(fullDelta, options = true) {
33
+ update(model, fullDelta, options = true) {
37
34
  if (options === false) {
38
35
  return;
39
36
  }
40
37
  if (options === true) {
41
38
  options = {};
42
39
  }
43
- if (this.disposed) {
44
- return;
45
- }
46
40
  //at max catch up to 1 second of physics in one update call (running at less then 1fps is unplayable anyways)
47
41
  fullDelta = Math.min(1, fullDelta);
48
42
  const updatesPerSecond = options.updatesPerSecond ?? 60;
@@ -52,10 +46,10 @@ export class BvhCharacterPhysics {
52
46
  const partialDelta = Math.min(fullDelta, physicsDelta);
53
47
  fullDelta -= physicsDelta;
54
48
  //compute global position and inverted parent matrix so that we can compute the position in global space and re-assign it to the local chracter space
55
- if (this.character.parent != null) {
56
- this.character.parent.updateWorldMatrix(true, false);
57
- position.copy(this.character.position).applyMatrix4(this.character.parent.matrixWorld);
58
- invertedParentMatrix.copy(this.character.parent.matrixWorld).invert();
49
+ if (model.parent != null) {
50
+ model.parent.updateWorldMatrix(true, false);
51
+ position.copy(model.position).applyMatrix4(model.parent.matrixWorld);
52
+ invertedParentMatrix.copy(model.parent.matrixWorld).invert();
59
53
  }
60
54
  else {
61
55
  invertedParentMatrix.identity();
@@ -70,7 +64,7 @@ export class BvhCharacterPhysics {
70
64
  this.notGroundedSeconds = 0;
71
65
  }
72
66
  if (!isGrounded || this.inputVelocity.lengthSq() > 0) {
73
- this.character.position.copy(collisionFreePosition).applyMatrix4(invertedParentMatrix);
67
+ model.position.copy(collisionFreePosition).applyMatrix4(invertedParentMatrix);
74
68
  }
75
69
  //compute new velocity
76
70
  // apply gravity
@@ -101,9 +95,6 @@ export class BvhCharacterPhysics {
101
95
  this.aabbox.min.addScalar(-this.radius);
102
96
  this.aabbox.max.addScalar(this.radius);
103
97
  }
104
- dispose() {
105
- this.disposed = true;
106
- }
107
98
  shapecastCapsule(position, maxGroundSlope, options) {
108
99
  this.updateBoundingShapes(options);
109
100
  let grounded = false;
@@ -0,0 +1,2 @@
1
+ export declare const DefaultCrossFadeDuration = 0.1;
2
+ export declare const DefaultJumDelay = 0.2;
@@ -0,0 +1,2 @@
1
+ export const DefaultCrossFadeDuration = 0.1;
2
+ export const DefaultJumDelay = 0.2;
@@ -0,0 +1,101 @@
1
+ import { Group, Object3D, Object3DEventMap, AnimationAction } from 'three';
2
+ import { CharacterAnimationOptions } from '../animation/index.js';
3
+ import { CharacterCameraBehavior, SimpleCharacterCameraBehaviorOptions } from '../camera.js';
4
+ import { Input, ScreenJoystickInputOptions, LocomotionKeyboardInputOptions, PointerCaptureInputOptions, PointerLockInputOptions, InputSystem } from '../input/index.js';
5
+ import { CharacterModelOptions, CharacterModel } from '../model/index.js';
6
+ import { BvhCharacterPhysicsOptions, BvhCharacterPhysics, BvhPhysicsWorld } from '../physics/index.js';
7
+ export type SimpleCharacterState = {
8
+ camera: Object3D;
9
+ model?: CharacterModel;
10
+ physics: BvhCharacterPhysics;
11
+ inputSystem: InputSystem;
12
+ lastJump: number;
13
+ };
14
+ export type SimpleCharacterMovementOptions = {
15
+ /**
16
+ * @default true
17
+ */
18
+ jump?: {
19
+ /**
20
+ * @default 0.2
21
+ */
22
+ delay?: number;
23
+ /**
24
+ * @default 0.1
25
+ */
26
+ bufferTime?: number;
27
+ /**
28
+ * @default 8
29
+ */
30
+ speed?: number;
31
+ } | boolean;
32
+ /**
33
+ * @default true
34
+ */
35
+ walk?: {
36
+ speed?: number;
37
+ } | boolean;
38
+ /**
39
+ * @default true
40
+ */
41
+ run?: {
42
+ speed?: number;
43
+ } | boolean;
44
+ };
45
+ export type SimpleCharacterAnimationOptions = {
46
+ readonly walk?: CharacterAnimationOptions;
47
+ readonly run?: CharacterAnimationOptions;
48
+ readonly idle?: CharacterAnimationOptions;
49
+ readonly jumpUp?: CharacterAnimationOptions;
50
+ readonly jumpLoop?: CharacterAnimationOptions;
51
+ readonly jumpDown?: CharacterAnimationOptions;
52
+ readonly jumpForward?: CharacterAnimationOptions;
53
+ /**
54
+ * @default "movement"
55
+ */
56
+ yawRotationBasdOn?: 'camera' | 'movement';
57
+ /**
58
+ * @default 10
59
+ */
60
+ maxYawRotationSpeed?: number;
61
+ /**
62
+ * @default 0.1
63
+ */
64
+ crossFadeDuration?: number;
65
+ };
66
+ export type SimpleCharacterInputOptions = ScreenJoystickInputOptions & PointerCaptureInputOptions & PointerLockInputOptions & LocomotionKeyboardInputOptions;
67
+ export type SimpleCharacterOptions = {
68
+ readonly input?: ReadonlyArray<Input | {
69
+ new (domElement: HTMLElement): Input;
70
+ }>;
71
+ inputOptions?: SimpleCharacterInputOptions;
72
+ movement?: SimpleCharacterMovementOptions;
73
+ readonly model?: CharacterModelOptions | boolean;
74
+ physics?: BvhCharacterPhysicsOptions;
75
+ cameraBehavior?: SimpleCharacterCameraBehaviorOptions;
76
+ readonly animation?: SimpleCharacterAnimationOptions;
77
+ };
78
+ export declare class SimpleCharacter extends Group<Object3DEventMap & {
79
+ loaded: {};
80
+ }> implements SimpleCharacterState {
81
+ readonly camera: Object3D;
82
+ private readonly world;
83
+ private readonly options;
84
+ readonly cameraBehavior: CharacterCameraBehavior;
85
+ readonly physics: BvhCharacterPhysics;
86
+ readonly inputSystem: InputSystem;
87
+ readonly currentAnimationRef: {
88
+ current?: AnimationAction;
89
+ };
90
+ model?: CharacterModel;
91
+ private readonly updateTimeline;
92
+ private readonly graph;
93
+ private readonly abortController;
94
+ lastJump: number;
95
+ constructor(camera: Object3D, world: BvhPhysicsWorld, domElement: HTMLElement, options?: SimpleCharacterOptions);
96
+ private init;
97
+ update(delta: number): void;
98
+ dispose(): void;
99
+ }
100
+ export * from './update-input-velocity.js';
101
+ export * from './update-rotation.js';
@@ -0,0 +1,109 @@
1
+ import { VRM, VRMUtils } from '@pixiv/three-vrm';
2
+ import { runTimeline, GraphTimeline } from '@pmndrs/timeline';
3
+ import { Group, Vector3 } from 'three';
4
+ import { CharacterCameraBehavior } from '../camera.js';
5
+ import { LocomotionKeyboardInput, PointerCaptureInput, ScreenJoystickInput, ScreenJumpButtonInput, InputSystem, } from '../input/index.js';
6
+ import { loadCharacterModel, flattenCharacterModelOptions, } from '../model/index.js';
7
+ import { BvhCharacterPhysics } from '../physics/index.js';
8
+ import { loadSimpleCharacterJumpDownState } from './state/jump-down.js';
9
+ import { loadSimpleCharacterJumpForwardAction, loadSimpleCharacterJumpForwardState } from './state/jump-forward.js';
10
+ import { loadSimpleCharacterJumpLoopState } from './state/jump-loop.js';
11
+ import { loadSimpleCharacterJumpStartState } from './state/jump-start.js';
12
+ import { loadSimpleCharacterJumpUpAction, loadSimpleCharacterJumpUpState } from './state/jump-up.js';
13
+ import { loadSimpleCharacterMovingState } from './state/movement.js';
14
+ import { updateSimpleCharacterInputVelocity } from './update-input-velocity.js';
15
+ import { updateSimpleCharacterRotation } from './update-rotation.js';
16
+ import { shouldJump } from '../utils.js';
17
+ export class SimpleCharacter extends Group {
18
+ camera;
19
+ world;
20
+ options;
21
+ cameraBehavior;
22
+ physics;
23
+ inputSystem;
24
+ currentAnimationRef = {};
25
+ //loaded asychronously
26
+ model;
27
+ updateTimeline;
28
+ graph = new GraphTimeline('moving');
29
+ abortController = new AbortController();
30
+ lastJump = 0;
31
+ constructor(camera, world, domElement, options = {}) {
32
+ super();
33
+ this.camera = camera;
34
+ this.world = world;
35
+ this.options = options;
36
+ this.inputSystem = new InputSystem();
37
+ const inputOptions = options.input ?? [
38
+ ScreenJoystickInput,
39
+ ScreenJumpButtonInput,
40
+ PointerCaptureInput,
41
+ LocomotionKeyboardInput,
42
+ ];
43
+ for (let input of inputOptions) {
44
+ this.inputSystem.add(typeof input === 'function' ? new input(domElement) : input);
45
+ }
46
+ // camera behavior
47
+ this.cameraBehavior = new CharacterCameraBehavior();
48
+ console.log(this.graph);
49
+ // physics
50
+ this.physics = new BvhCharacterPhysics(world);
51
+ // timeline graph
52
+ this.updateTimeline = runTimeline(this.graph.run(), this.abortController.signal);
53
+ //init resource loading
54
+ this.init(this.options).catch(console.error);
55
+ }
56
+ async init(options) {
57
+ if (options.model === false) {
58
+ return;
59
+ }
60
+ this.model = await loadCharacterModel(...flattenCharacterModelOptions(options.model === true ? undefined : options.model));
61
+ this.add(this.model.scene);
62
+ const [jumpForwardAction, jumpUpAction] = await Promise.all([
63
+ loadSimpleCharacterJumpForwardAction(this, options),
64
+ loadSimpleCharacterJumpUpAction(this, options),
65
+ ]);
66
+ await Promise.all([
67
+ loadSimpleCharacterJumpDownState(this, options).then((state) => this.graph.attach('jumpDown', state.timeline, state.transitionTo)),
68
+ loadSimpleCharacterJumpLoopState(this, options).then((state) => this.graph.attach('jumpLoop', state.timeline, state.transitionTo)),
69
+ ,
70
+ loadSimpleCharacterJumpForwardState(jumpForwardAction, this, options).then((state) => this.graph.attach('jumpForward', state.timeline, state.transitionTo)),
71
+ ,
72
+ loadSimpleCharacterJumpUpState(jumpUpAction, this, options).then((state) => this.graph.attach('jumpUp', state.timeline, state.transitionTo)),
73
+ ,
74
+ loadSimpleCharacterJumpStartState(jumpUpAction, jumpForwardAction, this, options).then((state) => this.graph.attach('jumpStart', state.timeline, state.transitionTo)),
75
+ ,
76
+ loadSimpleCharacterMovingState(this, options).then((state) => this.graph.attach('moving', state.timeline, state.transitionTo)),
77
+ ,
78
+ ]);
79
+ }
80
+ update(delta) {
81
+ const jumpOptions = this.options.movement?.jump;
82
+ if (jumpOptions != false &&
83
+ this.model == null &&
84
+ shouldJump(this.physics, this.inputSystem, this.lastJump, jumpOptions == true ? undefined : jumpOptions?.bufferTime)) {
85
+ this.physics.applyVelocity(new Vector3(0, (typeof this.options.movement?.jump === 'object' ? this.options.movement?.jump.speed : undefined) ?? 8, 0));
86
+ this.lastJump = performance.now() / 1000;
87
+ }
88
+ if (this.model != null) {
89
+ updateSimpleCharacterRotation(delta, this.physics, this.camera, this.model, this.options.animation);
90
+ }
91
+ updateSimpleCharacterInputVelocity(this.camera, this.inputSystem, this.physics, this.options.movement);
92
+ this.updateTimeline?.(undefined, delta);
93
+ this.model?.mixer.update(delta);
94
+ if (this.model instanceof VRM) {
95
+ this.model.update(delta);
96
+ }
97
+ this.physics.update(this, delta, this.options.physics);
98
+ this.cameraBehavior.update(this.camera, this, this.inputSystem, delta, this.world.raycast.bind(this.world), this.options.cameraBehavior);
99
+ }
100
+ dispose() {
101
+ this.abortController.abort();
102
+ this.parent?.remove(this);
103
+ this.model?.scene.dispatchEvent({ type: 'dispose' });
104
+ this.inputSystem.dispose();
105
+ VRMUtils.deepDispose(this);
106
+ }
107
+ }
108
+ export * from './update-input-velocity.js';
109
+ export * from './update-rotation.js';
@@ -0,0 +1,6 @@
1
+ export * from './jump-down.js';
2
+ export * from './jump-loop.js';
3
+ export * from './jump-start.js';
4
+ export * from './jump-forward.js';
5
+ export * from './jump-up.js';
6
+ export * from './movement.js';
@@ -0,0 +1,6 @@
1
+ export * from './jump-down.js';
2
+ export * from './jump-loop.js';
3
+ export * from './jump-start.js';
4
+ export * from './jump-forward.js';
5
+ export * from './jump-up.js';
6
+ export * from './movement.js';
@@ -0,0 +1,3 @@
1
+ import { type GraphTimelineState } from '@pmndrs/timeline';
2
+ import type { SimpleCharacterState, SimpleCharacterOptions } from '../index.js';
3
+ export declare function loadSimpleCharacterJumpDownState<T>(state: SimpleCharacterState, options: SimpleCharacterOptions): Promise<GraphTimelineState<T>>;
@@ -0,0 +1,25 @@
1
+ import { action, timePassed } from '@pmndrs/timeline';
2
+ import { LoopOnce } from 'three';
3
+ import { flattenCharacterAnimationOptions, loadCharacterAnimation } from '../../animation/index.js';
4
+ import { startAnimation } from '../../utils.js';
5
+ import { DefaultCrossFadeDuration } from '../defaults.js';
6
+ export async function loadSimpleCharacterJumpDownState(state, options) {
7
+ const model = state.model;
8
+ if (model == null) {
9
+ throw new Error(`Unable to load animation without existing model`);
10
+ }
11
+ const jumpDown = model.mixer.clipAction(await loadCharacterAnimation(model, ...flattenCharacterAnimationOptions(options.animation?.jumpDown ?? { url: { default: 'jumpDown' } })));
12
+ jumpDown.loop = LoopOnce;
13
+ jumpDown.clampWhenFinished = true;
14
+ return {
15
+ timeline: () => action({
16
+ init: () => {
17
+ startAnimation(jumpDown, model.currentAnimations, {
18
+ fadeDuration: options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration,
19
+ });
20
+ },
21
+ until: timePassed(150, 'milliseconds'),
22
+ }),
23
+ transitionTo: { finally: 'moving' },
24
+ };
25
+ }
@@ -0,0 +1,5 @@
1
+ import { type GraphTimelineState } from '@pmndrs/timeline';
2
+ import { AnimationAction } from 'three';
3
+ import { SimpleCharacterState, SimpleCharacterOptions } from '../index.js';
4
+ export declare function loadSimpleCharacterJumpForwardAction(state: SimpleCharacterState, options: SimpleCharacterOptions): Promise<AnimationAction>;
5
+ export declare function loadSimpleCharacterJumpForwardState<T>(jumpForward: AnimationAction, state: SimpleCharacterState, options: SimpleCharacterOptions): Promise<GraphTimelineState<T>>;
@@ -0,0 +1,39 @@
1
+ import { action, animationFinished } from '@pmndrs/timeline';
2
+ import { LoopOnce, Vector3 } from 'three';
3
+ import { flattenCharacterAnimationOptions, loadCharacterAnimation } from '../../animation/index.js';
4
+ import { startAnimation } from '../../utils.js';
5
+ import { DefaultCrossFadeDuration } from '../defaults.js';
6
+ export async function loadSimpleCharacterJumpForwardAction(state, options) {
7
+ const model = state.model;
8
+ if (model == null) {
9
+ throw new Error(`Unable to load animation without existing model`);
10
+ }
11
+ const jumpForward = model.mixer.clipAction(await loadCharacterAnimation(model, ...flattenCharacterAnimationOptions(options.animation?.jumpForward ?? {
12
+ url: { default: 'jumpForward' },
13
+ scaleTime: 0.9,
14
+ })));
15
+ jumpForward.loop = LoopOnce;
16
+ jumpForward.clampWhenFinished = true;
17
+ return jumpForward;
18
+ }
19
+ export async function loadSimpleCharacterJumpForwardState(jumpForward, state, options) {
20
+ const model = state.model;
21
+ if (model == null) {
22
+ throw new Error(`Unable to load animation state without existing model`);
23
+ }
24
+ return {
25
+ timeline: () => action({
26
+ init: () => {
27
+ startAnimation(jumpForward, model.currentAnimations, {
28
+ fadeDuration: options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration,
29
+ });
30
+ state.lastJump = performance.now() / 1000;
31
+ state.physics.applyVelocity(new Vector3(0, (typeof options.movement?.jump === 'object' ? options.movement?.jump.speed : undefined) ?? 8, 0));
32
+ },
33
+ until: animationFinished(jumpForward),
34
+ }),
35
+ transitionTo: {
36
+ finally: () => (state.physics.isGrounded ? 'moving' : 'jumpLoop'),
37
+ },
38
+ };
39
+ }
@@ -0,0 +1,3 @@
1
+ import { type GraphTimelineState } from '@pmndrs/timeline';
2
+ import type { SimpleCharacterOptions, SimpleCharacterState } from '../index.js';
3
+ export declare function loadSimpleCharacterJumpLoopState<T>(state: SimpleCharacterState, options: SimpleCharacterOptions): Promise<GraphTimelineState<T>>;
@@ -0,0 +1,23 @@
1
+ import { action } from '@pmndrs/timeline';
2
+ import { flattenCharacterAnimationOptions, loadCharacterAnimation } from '../../animation/index.js';
3
+ import { startAnimation } from '../../utils.js';
4
+ import { DefaultCrossFadeDuration } from '../defaults.js';
5
+ export async function loadSimpleCharacterJumpLoopState(state, options) {
6
+ const model = state.model;
7
+ if (model == null) {
8
+ throw new Error(`Unable to load animation without existing model`);
9
+ }
10
+ const jumpLoop = model.mixer.clipAction(await loadCharacterAnimation(model, ...flattenCharacterAnimationOptions(options.animation?.jumpLoop ?? { url: { default: 'jumpLoop' } })));
11
+ return {
12
+ timeline: () => action({
13
+ init: () => {
14
+ startAnimation(jumpLoop, model.currentAnimations, {
15
+ fadeDuration: options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration,
16
+ });
17
+ },
18
+ }),
19
+ transitionTo: {
20
+ jumpDown: { whenUpdate: () => state.physics.isGrounded },
21
+ },
22
+ };
23
+ }
@@ -0,0 +1,4 @@
1
+ import { GraphTimelineState } from '@pmndrs/timeline';
2
+ import { AnimationAction } from 'three';
3
+ import type { SimpleCharacterOptions, SimpleCharacterState } from '../index.js';
4
+ export declare function loadSimpleCharacterJumpStartState<T>(jumpUp: AnimationAction, jumpForward: AnimationAction, state: SimpleCharacterState, options: SimpleCharacterOptions): Promise<GraphTimelineState<T>>;