@pmndrs/viverse 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/animation/index.js +12 -8
- package/dist/camera.d.ts +2 -2
- package/dist/camera.js +9 -9
- package/dist/index.d.ts +1 -0
- package/dist/index.js +14 -0
- package/dist/input/index.d.ts +7 -5
- package/dist/input/index.js +23 -11
- package/dist/input/keyboard.d.ts +11 -2
- package/dist/input/keyboard.js +20 -13
- package/dist/input/pointer-capture.d.ts +8 -2
- package/dist/input/pointer-capture.js +43 -8
- package/dist/input/pointer-lock.d.ts +6 -2
- package/dist/input/pointer-lock.js +7 -5
- package/dist/input/screen-joystick.d.ts +22 -0
- package/dist/input/screen-joystick.js +127 -0
- package/dist/input/screen-jump-button.d.ts +8 -0
- package/dist/input/screen-jump-button.js +49 -0
- package/dist/physics/index.d.ts +2 -2
- package/dist/physics/index.js +4 -4
- package/dist/physics/world.js +1 -1
- package/dist/simple-character.d.ts +6 -5
- package/dist/simple-character.js +21 -9
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +14 -0
- package/package.json +1 -1
package/dist/animation/index.js
CHANGED
|
@@ -5,10 +5,8 @@ import { loadVrmModelMixamoAnimations as loadModelMixamoAnimations } from './mix
|
|
|
5
5
|
import { scaleAnimationClipTime, trimAnimationClip } from './utils.js';
|
|
6
6
|
import { loadVrmModelVrmaAnimations } from './vrma.js';
|
|
7
7
|
import { cached } from '../utils.js';
|
|
8
|
-
const parentWorldVector = new Vector3();
|
|
9
8
|
const restRotationInverse = new Quaternion();
|
|
10
9
|
const parentRestWorldRotation = new Quaternion();
|
|
11
|
-
const parentRestWorldRotationInverse = new Quaternion();
|
|
12
10
|
const quaternion = new Quaternion();
|
|
13
11
|
const vector = new Vector3();
|
|
14
12
|
const nonVrmRotationOffset = new Quaternion().setFromEuler(new Euler(0, Math.PI, 0));
|
|
@@ -18,7 +16,9 @@ export function fixModelAnimationClip(model, clip, clipScene, removeXZMovement,
|
|
|
18
16
|
throw new Error('Failed to determine hips bone name for VRM animation. Please check the bone map or animation file.');
|
|
19
17
|
}
|
|
20
18
|
const clipSceneHips = clipScene.getObjectByName(hipsBoneName);
|
|
21
|
-
const vrmHipsPosition = model instanceof VRM
|
|
19
|
+
const vrmHipsPosition = model instanceof VRM
|
|
20
|
+
? model.humanoid.normalizedRestPose.hips?.position
|
|
21
|
+
: model.scene.getObjectByName('hips')?.position.toArray();
|
|
22
22
|
if (clipSceneHips == null || vrmHipsPosition == null) {
|
|
23
23
|
throw new Error('Failed to load VRM animation: missing animation hips object or VRM hips position.');
|
|
24
24
|
}
|
|
@@ -32,14 +32,18 @@ export function fixModelAnimationClip(model, clip, clipScene, removeXZMovement,
|
|
|
32
32
|
const vrmBoneName = boneMap?.[boneName] ?? boneName;
|
|
33
33
|
const vrmNodeName = model instanceof VRM ? model.humanoid.getNormalizedBoneNode(vrmBoneName)?.name : vrmBoneName;
|
|
34
34
|
const bone = clipScene.getObjectByName(boneName);
|
|
35
|
-
if (vrmNodeName == null || bone == null
|
|
35
|
+
if (vrmNodeName == null || bone == null) {
|
|
36
36
|
continue;
|
|
37
37
|
}
|
|
38
|
-
bone.getWorldQuaternion(restRotationInverse).invert();
|
|
39
|
-
bone.parent.getWorldQuaternion(parentRestWorldRotation);
|
|
40
|
-
parentRestWorldRotationInverse.copy(parentRestWorldRotation).invert();
|
|
41
|
-
bone.parent.getWorldPosition(parentWorldVector);
|
|
42
38
|
if (track instanceof QuaternionKeyframeTrack) {
|
|
39
|
+
if (bone.parent != null) {
|
|
40
|
+
bone.getWorldQuaternion(restRotationInverse).invert();
|
|
41
|
+
bone.parent.getWorldQuaternion(parentRestWorldRotation);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
restRotationInverse.identity();
|
|
45
|
+
parentRestWorldRotation.identity();
|
|
46
|
+
}
|
|
43
47
|
// Store rotations of rest-pose.
|
|
44
48
|
for (let i = 0; i < track.values.length; i += 4) {
|
|
45
49
|
quaternion.fromArray(track.values, i);
|
package/dist/camera.d.ts
CHANGED
|
@@ -56,7 +56,7 @@ export type SimpleCharacterCameraBehaviorOptions = {
|
|
|
56
56
|
} | boolean;
|
|
57
57
|
} | boolean;
|
|
58
58
|
export declare class SimpleCharacterCameraBehavior {
|
|
59
|
-
|
|
59
|
+
getCamera: () => Object3D;
|
|
60
60
|
character: SimpleCharacter;
|
|
61
61
|
private readonly raycast?;
|
|
62
62
|
rotationPitch: number;
|
|
@@ -64,7 +64,7 @@ export declare class SimpleCharacterCameraBehavior {
|
|
|
64
64
|
zoomDistance: number;
|
|
65
65
|
private collisionFreeZoomDistance;
|
|
66
66
|
private firstUpdate;
|
|
67
|
-
constructor(
|
|
67
|
+
constructor(getCamera: () => Object3D, character: SimpleCharacter, raycast?: ((ray: Ray, far: number) => number | undefined) | undefined);
|
|
68
68
|
private setRotationFromDelta;
|
|
69
69
|
private setDistanceFromDelta;
|
|
70
70
|
private computeCharacterBaseOffset;
|
package/dist/camera.js
CHANGED
|
@@ -12,7 +12,7 @@ const characterWorldPosition = new Vector3();
|
|
|
12
12
|
const euler = new Euler();
|
|
13
13
|
const rayHelper = new Ray();
|
|
14
14
|
export class SimpleCharacterCameraBehavior {
|
|
15
|
-
|
|
15
|
+
getCamera;
|
|
16
16
|
character;
|
|
17
17
|
raycast;
|
|
18
18
|
rotationPitch = (-20 * Math.PI) / 180;
|
|
@@ -21,15 +21,15 @@ export class SimpleCharacterCameraBehavior {
|
|
|
21
21
|
//internal state
|
|
22
22
|
collisionFreeZoomDistance = this.zoomDistance;
|
|
23
23
|
firstUpdate = true;
|
|
24
|
-
constructor(
|
|
25
|
-
this.
|
|
24
|
+
constructor(getCamera, character, raycast) {
|
|
25
|
+
this.getCamera = getCamera;
|
|
26
26
|
this.character = character;
|
|
27
27
|
this.raycast = raycast;
|
|
28
28
|
}
|
|
29
29
|
setRotationFromDelta(delta, rotationOptions) {
|
|
30
30
|
if (delta.lengthSq() < 0.0001) {
|
|
31
31
|
// use current camera rotation if very close to target
|
|
32
|
-
euler.setFromQuaternion(this.
|
|
32
|
+
euler.setFromQuaternion(this.getCamera().quaternion, 'YXZ');
|
|
33
33
|
this.rotationPitch = euler.x;
|
|
34
34
|
this.rotationYaw = euler.y;
|
|
35
35
|
return;
|
|
@@ -76,7 +76,7 @@ export class SimpleCharacterCameraBehavior {
|
|
|
76
76
|
this.computeCharacterBaseOffset(chracterBaseOffsetHelper, options.characterBaseOffset);
|
|
77
77
|
this.character.getWorldPosition(characterWorldPosition);
|
|
78
78
|
characterWorldPosition.add(chracterBaseOffsetHelper);
|
|
79
|
-
this.
|
|
79
|
+
this.getCamera().getWorldPosition(deltaHelper);
|
|
80
80
|
deltaHelper.sub(characterWorldPosition);
|
|
81
81
|
// apply rotation input to rotationYaw and rotationPitch if not disabled or first update
|
|
82
82
|
let rotationOptions = options.rotation ?? true;
|
|
@@ -92,8 +92,8 @@ export class SimpleCharacterCameraBehavior {
|
|
|
92
92
|
this.setRotationFromDelta(deltaHelper, typeof rotationOptions === 'boolean' ? {} : rotationOptions);
|
|
93
93
|
}
|
|
94
94
|
// apply yaw and pitch to camera rotation
|
|
95
|
-
this.
|
|
96
|
-
rayHelper.direction.set(0, 0, 1).applyEuler(this.
|
|
95
|
+
this.getCamera().rotation.set(this.rotationPitch, this.rotationYaw, 0, 'YXZ');
|
|
96
|
+
rayHelper.direction.set(0, 0, 1).applyEuler(this.getCamera().rotation);
|
|
97
97
|
rayHelper.origin.copy(characterWorldPosition);
|
|
98
98
|
// apply zoom input to zoomDistance if not disabled or first update
|
|
99
99
|
let zoomOptions = options.zoom ?? true;
|
|
@@ -126,12 +126,12 @@ export class SimpleCharacterCameraBehavior {
|
|
|
126
126
|
}
|
|
127
127
|
// Calculate camera position using spherical coordinates from euler
|
|
128
128
|
sphericalOffset.set(0, 0, this.collisionFreeZoomDistance);
|
|
129
|
-
sphericalOffset.applyEuler(this.
|
|
129
|
+
sphericalOffset.applyEuler(this.getCamera().rotation);
|
|
130
130
|
// Get target position with offset (reuse helper vector)
|
|
131
131
|
this.character.getWorldPosition(characterWorldPosition);
|
|
132
132
|
this.computeCharacterBaseOffset(chracterBaseOffsetHelper, options.characterBaseOffset);
|
|
133
133
|
characterWorldPosition.add(chracterBaseOffsetHelper);
|
|
134
134
|
// Set camera position relative to target
|
|
135
|
-
this.
|
|
135
|
+
this.getCamera().position.copy(characterWorldPosition).add(sphericalOffset);
|
|
136
136
|
}
|
|
137
137
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { extractProxy, getIsMobileMediaQuery, isMobile } from './utils.js';
|
|
1
2
|
export * from './input/index.js';
|
|
2
3
|
export * from './camera.js';
|
|
3
4
|
export * from './physics/index.js';
|
|
@@ -5,3 +6,16 @@ export * from './animation/index.js';
|
|
|
5
6
|
export * from './material.js';
|
|
6
7
|
export * from './simple-character.js';
|
|
7
8
|
export * from './model/index.js';
|
|
9
|
+
(function injectMobileClassStyle() {
|
|
10
|
+
if (typeof document === 'undefined') {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const STYLE_ID = 'viverse-mobile-class-style';
|
|
14
|
+
if (document.getElementById(STYLE_ID)) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const style = document.createElement('style');
|
|
18
|
+
style.id = STYLE_ID;
|
|
19
|
+
style.textContent = `.mobile-only{display:none;}@media (hover: none) and (pointer: coarse){.mobile-only{display:unset;}}`;
|
|
20
|
+
document.head.appendChild(style);
|
|
21
|
+
})();
|
package/dist/input/index.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
export declare class InputSystem {
|
|
2
|
-
enabled: boolean;
|
|
3
2
|
private readonly inputs;
|
|
4
3
|
constructor(domElement: HTMLElement, inputs: ReadonlyArray<Input | {
|
|
5
|
-
new (element: HTMLElement): Input;
|
|
6
|
-
}
|
|
4
|
+
new (element: HTMLElement, options?: {}): Input;
|
|
5
|
+
}>, options?: {});
|
|
7
6
|
add(input: Input): void;
|
|
8
7
|
remove(input: Input): void;
|
|
9
|
-
|
|
8
|
+
dispose(): void;
|
|
10
9
|
get<T>(field: InputField<T>): T;
|
|
11
10
|
}
|
|
12
11
|
export type InputField<T> = {
|
|
13
12
|
default: T;
|
|
13
|
+
combine: (v1: any, v2: any) => T;
|
|
14
14
|
};
|
|
15
15
|
export declare const MoveForwardField: InputField<number>;
|
|
16
16
|
export declare const MoveBackwardField: InputField<number>;
|
|
@@ -23,8 +23,10 @@ export declare const DeltaYawField: InputField<number>;
|
|
|
23
23
|
export declare const DeltaPitchField: InputField<number>;
|
|
24
24
|
export interface Input {
|
|
25
25
|
get<T>(field: InputField<T>): T | undefined;
|
|
26
|
-
|
|
26
|
+
dispose?(): void;
|
|
27
27
|
}
|
|
28
28
|
export * from './pointer-lock.js';
|
|
29
29
|
export * from './pointer-capture.js';
|
|
30
30
|
export * from './keyboard.js';
|
|
31
|
+
export * from './screen-joystick.js';
|
|
32
|
+
export * from './screen-jump-button.js';
|
package/dist/input/index.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
export class InputSystem {
|
|
2
|
-
enabled = true;
|
|
3
2
|
inputs;
|
|
4
|
-
constructor(domElement, inputs) {
|
|
5
|
-
this.inputs = inputs.map((input) => (typeof input === 'function' ? new input(domElement) : input));
|
|
3
|
+
constructor(domElement, inputs, options) {
|
|
4
|
+
this.inputs = inputs.map((input) => (typeof input === 'function' ? new input(domElement, options) : input));
|
|
6
5
|
}
|
|
7
6
|
add(input) {
|
|
8
7
|
this.inputs.push(input);
|
|
@@ -14,51 +13,64 @@ export class InputSystem {
|
|
|
14
13
|
}
|
|
15
14
|
this.inputs.splice(index, 1);
|
|
16
15
|
}
|
|
17
|
-
|
|
18
|
-
this.inputs.forEach((input) => input.
|
|
16
|
+
dispose() {
|
|
17
|
+
this.inputs.forEach((input) => input.dispose?.());
|
|
19
18
|
this.inputs.length = 0;
|
|
20
19
|
}
|
|
21
20
|
get(field) {
|
|
22
|
-
|
|
23
|
-
return field.default;
|
|
24
|
-
}
|
|
21
|
+
let current;
|
|
25
22
|
for (const input of this.inputs) {
|
|
26
23
|
const result = input.get(field);
|
|
27
|
-
if (result
|
|
24
|
+
if (result == null) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (current == undefined) {
|
|
28
|
+
current = result;
|
|
28
29
|
continue;
|
|
29
30
|
}
|
|
30
|
-
|
|
31
|
+
current = field.combine(current, result);
|
|
31
32
|
}
|
|
32
|
-
return field.default;
|
|
33
|
+
return current ?? field.default;
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
export const MoveForwardField = {
|
|
36
37
|
default: 0,
|
|
38
|
+
combine: Math.max,
|
|
37
39
|
};
|
|
38
40
|
export const MoveBackwardField = {
|
|
39
41
|
default: 0,
|
|
42
|
+
combine: Math.max,
|
|
40
43
|
};
|
|
41
44
|
export const MoveLeftField = {
|
|
42
45
|
default: 0,
|
|
46
|
+
combine: Math.max,
|
|
43
47
|
};
|
|
44
48
|
export const MoveRightField = {
|
|
45
49
|
default: 0,
|
|
50
|
+
combine: Math.max,
|
|
46
51
|
};
|
|
47
52
|
export const LastTimeJumpPressedField = {
|
|
48
53
|
default: null,
|
|
54
|
+
combine: (v1, v2) => (v1 == null && v2 == null ? null : Math.min(v1 ?? Infinity, v2 ?? Infinity)),
|
|
49
55
|
};
|
|
50
56
|
export const RunField = {
|
|
51
57
|
default: false,
|
|
58
|
+
combine: (v1, v2) => v1 || v2,
|
|
52
59
|
};
|
|
53
60
|
export const DeltaZoomField = {
|
|
54
61
|
default: 0,
|
|
62
|
+
combine: Math.max,
|
|
55
63
|
};
|
|
56
64
|
export const DeltaYawField = {
|
|
57
65
|
default: 0,
|
|
66
|
+
combine: Math.max,
|
|
58
67
|
};
|
|
59
68
|
export const DeltaPitchField = {
|
|
60
69
|
default: 0,
|
|
70
|
+
combine: Math.max,
|
|
61
71
|
};
|
|
62
72
|
export * from './pointer-lock.js';
|
|
63
73
|
export * from './pointer-capture.js';
|
|
64
74
|
export * from './keyboard.js';
|
|
75
|
+
export * from './screen-joystick.js';
|
|
76
|
+
export * from './screen-jump-button.js';
|
package/dist/input/keyboard.d.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { Input, InputField } from './index.js';
|
|
2
|
+
export type LocomotionKeyboardInputOptions = {
|
|
3
|
+
keyboardMoveForwardKeys?: Array<string>;
|
|
4
|
+
keyboardMoveBackwardKeys?: Array<string>;
|
|
5
|
+
keyboardMoveLeftKeys?: Array<string>;
|
|
6
|
+
keyboardMoveRightKeys?: Array<string>;
|
|
7
|
+
keyboardRunKeys?: Array<string>;
|
|
8
|
+
keyboardJumpKeys?: Array<string>;
|
|
9
|
+
};
|
|
2
10
|
export declare class LocomotionKeyboardInput implements Input {
|
|
11
|
+
private readonly options;
|
|
3
12
|
private readonly abortController;
|
|
4
13
|
private readonly keyState;
|
|
5
|
-
constructor(domElement: HTMLElement);
|
|
14
|
+
constructor(domElement: HTMLElement, options?: LocomotionKeyboardInputOptions);
|
|
6
15
|
get<T>(field: InputField<T>): T | undefined;
|
|
7
|
-
|
|
16
|
+
dispose(): void;
|
|
8
17
|
}
|
package/dist/input/keyboard.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { MoveForwardField, MoveBackwardField, MoveLeftField, MoveRightField, LastTimeJumpPressedField, RunField, } from './index.js';
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
2
|
+
const DefaultMoveForwardKeys = ['KeyW'];
|
|
3
|
+
const DefaultMoveBackwardKeys = ['KeyS'];
|
|
4
|
+
const DefaultMoveLeftKeys = ['KeyA'];
|
|
5
|
+
const DefaultMoveRightKeys = ['KeyD'];
|
|
6
|
+
const DefaultRunKeys = ['ShiftRight', 'ShiftLeft'];
|
|
7
|
+
const DefaultJumpKeys = ['Space'];
|
|
7
8
|
export class LocomotionKeyboardInput {
|
|
9
|
+
options;
|
|
8
10
|
abortController = new AbortController();
|
|
9
11
|
keyState = new Map();
|
|
10
|
-
constructor(domElement) {
|
|
12
|
+
constructor(domElement, options = {}) {
|
|
13
|
+
this.options = options;
|
|
11
14
|
domElement.tabIndex = 0;
|
|
12
15
|
domElement.addEventListener('keydown', (event) => {
|
|
13
16
|
let state = this.keyState.get(event.code);
|
|
@@ -40,24 +43,28 @@ export class LocomotionKeyboardInput {
|
|
|
40
43
|
}
|
|
41
44
|
get(field) {
|
|
42
45
|
if (field === LastTimeJumpPressedField) {
|
|
43
|
-
|
|
46
|
+
const jumpKeys = this.options.keyboardJumpKeys ?? DefaultJumpKeys;
|
|
47
|
+
const pressed = jumpKeys
|
|
48
|
+
.map((key) => this.keyState.get(key)?.pressTime ?? null)
|
|
49
|
+
.filter((t) => t != null);
|
|
50
|
+
return (pressed.length > 0 ? Math.max(...pressed) : null);
|
|
44
51
|
}
|
|
45
52
|
let keys;
|
|
46
53
|
switch (field) {
|
|
47
54
|
case MoveForwardField:
|
|
48
|
-
keys =
|
|
55
|
+
keys = this.options.keyboardMoveForwardKeys ?? DefaultMoveForwardKeys;
|
|
49
56
|
break;
|
|
50
57
|
case MoveBackwardField:
|
|
51
|
-
keys =
|
|
58
|
+
keys = this.options.keyboardMoveBackwardKeys ?? DefaultMoveBackwardKeys;
|
|
52
59
|
break;
|
|
53
60
|
case MoveLeftField:
|
|
54
|
-
keys =
|
|
61
|
+
keys = this.options.keyboardMoveLeftKeys ?? DefaultMoveLeftKeys;
|
|
55
62
|
break;
|
|
56
63
|
case MoveRightField:
|
|
57
|
-
keys =
|
|
64
|
+
keys = this.options.keyboardMoveRightKeys ?? DefaultMoveRightKeys;
|
|
58
65
|
break;
|
|
59
66
|
case RunField:
|
|
60
|
-
keys =
|
|
67
|
+
keys = this.options.keyboardRunKeys ?? DefaultRunKeys;
|
|
61
68
|
break;
|
|
62
69
|
}
|
|
63
70
|
if (keys == null) {
|
|
@@ -71,7 +78,7 @@ export class LocomotionKeyboardInput {
|
|
|
71
78
|
return state.releaseTime == null || state.pressTime > state.releaseTime;
|
|
72
79
|
});
|
|
73
80
|
}
|
|
74
|
-
|
|
81
|
+
dispose() {
|
|
75
82
|
this.abortController.abort();
|
|
76
83
|
}
|
|
77
84
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { Input, InputField } from './index.js';
|
|
2
|
+
export type PointerCaptureInputOptions = {
|
|
3
|
+
pointerCaptureRotationSpeed?: number;
|
|
4
|
+
pointerCaptureZoomSpeed?: number;
|
|
5
|
+
};
|
|
2
6
|
/**
|
|
3
7
|
* @requires to manually execute `domElement.setPointerCapture(pointerId)` on pointerdown
|
|
4
8
|
*/
|
|
@@ -8,7 +12,9 @@ export declare class PointerCaptureInput implements Input {
|
|
|
8
12
|
private deltaZoom;
|
|
9
13
|
private deltaYaw;
|
|
10
14
|
private deltaPitch;
|
|
11
|
-
|
|
15
|
+
private activePointers;
|
|
16
|
+
private lastPinchDist;
|
|
17
|
+
constructor(domElement: HTMLElement, options?: PointerCaptureInputOptions);
|
|
12
18
|
get<T>(field: InputField<T>): T | undefined;
|
|
13
|
-
|
|
19
|
+
dispose(): void;
|
|
14
20
|
}
|
|
@@ -8,29 +8,64 @@ export class PointerCaptureInput {
|
|
|
8
8
|
deltaZoom = 0;
|
|
9
9
|
deltaYaw = 0;
|
|
10
10
|
deltaPitch = 0;
|
|
11
|
-
|
|
11
|
+
activePointers = new Map();
|
|
12
|
+
lastPinchDist = null;
|
|
13
|
+
constructor(domElement, options = {}) {
|
|
12
14
|
this.domElement = domElement;
|
|
13
|
-
domElement.addEventListener('pointerdown', (event) =>
|
|
15
|
+
domElement.addEventListener('pointerdown', (event) => {
|
|
16
|
+
this.domElement.setPointerCapture(event.pointerId);
|
|
17
|
+
this.activePointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
18
|
+
if (this.activePointers.size === 2) {
|
|
19
|
+
const pts = Array.from(this.activePointers.values());
|
|
20
|
+
this.lastPinchDist = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
|
|
21
|
+
}
|
|
22
|
+
}, {
|
|
14
23
|
signal: this.abortController.signal,
|
|
15
24
|
});
|
|
16
25
|
domElement.addEventListener('pointermove', (event) => {
|
|
17
26
|
if (!this.domElement.hasPointerCapture(event.pointerId)) {
|
|
18
27
|
return;
|
|
19
28
|
}
|
|
20
|
-
this.
|
|
21
|
-
|
|
29
|
+
this.activePointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
30
|
+
if (this.activePointers.size === 2) {
|
|
31
|
+
const pts = Array.from(this.activePointers.values());
|
|
32
|
+
if (this.lastPinchDist != null) {
|
|
33
|
+
const d = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
|
|
34
|
+
const zoomSpeed = options.pointerCaptureZoomSpeed ?? 0.0001;
|
|
35
|
+
this.deltaZoom += (this.lastPinchDist - d) * zoomSpeed;
|
|
36
|
+
this.lastPinchDist = d;
|
|
37
|
+
}
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const rotationSpeed = options.pointerCaptureRotationSpeed ?? 0.4;
|
|
42
|
+
this.deltaYaw -= (rotationSpeed * event.movementX) / window.innerHeight;
|
|
43
|
+
this.deltaPitch -= (rotationSpeed * event.movementY) / window.innerHeight;
|
|
22
44
|
}, {
|
|
23
45
|
signal: this.abortController.signal,
|
|
24
46
|
});
|
|
25
|
-
domElement.addEventListener('pointerup', (event) =>
|
|
47
|
+
domElement.addEventListener('pointerup', (event) => {
|
|
48
|
+
this.domElement.releasePointerCapture(event.pointerId);
|
|
49
|
+
this.activePointers.delete(event.pointerId);
|
|
50
|
+
if (this.activePointers.size < 2) {
|
|
51
|
+
this.lastPinchDist = null;
|
|
52
|
+
}
|
|
53
|
+
}, {
|
|
26
54
|
signal: this.abortController.signal,
|
|
27
55
|
});
|
|
28
|
-
domElement.addEventListener('pointercancel', (event) =>
|
|
56
|
+
domElement.addEventListener('pointercancel', (event) => {
|
|
57
|
+
this.domElement.releasePointerCapture(event.pointerId);
|
|
58
|
+
this.activePointers.delete(event.pointerId);
|
|
59
|
+
if (this.activePointers.size < 2) {
|
|
60
|
+
this.lastPinchDist = null;
|
|
61
|
+
}
|
|
62
|
+
}, {
|
|
29
63
|
signal: this.abortController.signal,
|
|
30
64
|
});
|
|
31
65
|
domElement.addEventListener('wheel', (event) => {
|
|
32
66
|
event.preventDefault();
|
|
33
|
-
|
|
67
|
+
const zoomSpeed = options.pointerCaptureZoomSpeed ?? 0.0001;
|
|
68
|
+
this.deltaZoom += event.deltaY * zoomSpeed;
|
|
34
69
|
}, {
|
|
35
70
|
signal: this.abortController.signal,
|
|
36
71
|
});
|
|
@@ -53,7 +88,7 @@ export class PointerCaptureInput {
|
|
|
53
88
|
}
|
|
54
89
|
return result;
|
|
55
90
|
}
|
|
56
|
-
|
|
91
|
+
dispose() {
|
|
57
92
|
this.abortController.abort();
|
|
58
93
|
}
|
|
59
94
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { Input, InputField } from './index.js';
|
|
2
|
+
export type PointerLockInputOptions = {
|
|
3
|
+
pointerLockRotationSpeed?: number;
|
|
4
|
+
pointerLockZoomSpeed?: number;
|
|
5
|
+
};
|
|
2
6
|
/**
|
|
3
7
|
* @requires to manually execute `domElement.requestPointerLock()`
|
|
4
8
|
*/
|
|
@@ -7,7 +11,7 @@ export declare class PointerLockInput implements Input {
|
|
|
7
11
|
private deltaZoom;
|
|
8
12
|
private deltaYaw;
|
|
9
13
|
private deltaPitch;
|
|
10
|
-
constructor(domElement: HTMLElement);
|
|
14
|
+
constructor(domElement: HTMLElement, options?: PointerLockInputOptions);
|
|
11
15
|
get<T>(field: InputField<T>): T | undefined;
|
|
12
|
-
|
|
16
|
+
dispose(): void;
|
|
13
17
|
}
|
|
@@ -7,15 +7,16 @@ export class PointerLockInput {
|
|
|
7
7
|
deltaZoom = 0;
|
|
8
8
|
deltaYaw = 0;
|
|
9
9
|
deltaPitch = 0;
|
|
10
|
-
constructor(domElement) {
|
|
10
|
+
constructor(domElement, options = {}) {
|
|
11
11
|
domElement.addEventListener('pointermove', (event) => {
|
|
12
12
|
if (document.pointerLockElement != domElement) {
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
|
+
const rotationSpeed = options.pointerLockRotationSpeed ?? 0.4;
|
|
15
16
|
// Compute based on domElement bounds instead of window.innerHeight
|
|
16
17
|
const rect = domElement.getBoundingClientRect();
|
|
17
|
-
this.deltaYaw -= (
|
|
18
|
-
this.deltaPitch -= (
|
|
18
|
+
this.deltaYaw -= (rotationSpeed * event.movementX) / rect.height;
|
|
19
|
+
this.deltaPitch -= (rotationSpeed * event.movementY) / rect.height;
|
|
19
20
|
}, {
|
|
20
21
|
signal: this.abortController.signal,
|
|
21
22
|
});
|
|
@@ -23,7 +24,8 @@ export class PointerLockInput {
|
|
|
23
24
|
if (document.pointerLockElement != domElement) {
|
|
24
25
|
return;
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
+
const zoomSpeed = options.pointerLockZoomSpeed ?? 0.0001;
|
|
28
|
+
this.deltaZoom += event.deltaY * zoomSpeed;
|
|
27
29
|
event.preventDefault();
|
|
28
30
|
}, {
|
|
29
31
|
signal: this.abortController.signal,
|
|
@@ -47,7 +49,7 @@ export class PointerLockInput {
|
|
|
47
49
|
}
|
|
48
50
|
return result;
|
|
49
51
|
}
|
|
50
|
-
|
|
52
|
+
dispose() {
|
|
51
53
|
this.abortController.abort();
|
|
52
54
|
}
|
|
53
55
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Input, InputField } from './index.js';
|
|
2
|
+
export type ScreenJoystickInputOptions = {
|
|
3
|
+
screenJoystickRunDistancePx?: number;
|
|
4
|
+
screenJoystickDeadZonePx?: number;
|
|
5
|
+
};
|
|
6
|
+
export declare class ScreenJoystickInput implements Input {
|
|
7
|
+
private readonly options;
|
|
8
|
+
readonly root: HTMLDivElement;
|
|
9
|
+
private readonly handle;
|
|
10
|
+
private moveX;
|
|
11
|
+
private moveY;
|
|
12
|
+
private running;
|
|
13
|
+
private readonly joystickRadius;
|
|
14
|
+
private joyCenterX;
|
|
15
|
+
private joyCenterY;
|
|
16
|
+
private pointerId;
|
|
17
|
+
constructor(domElement: HTMLElement, options?: ScreenJoystickInputOptions);
|
|
18
|
+
get<T>(field: InputField<T>): T | undefined;
|
|
19
|
+
dispose(): void;
|
|
20
|
+
private updateHandle;
|
|
21
|
+
private resetHandle;
|
|
22
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { MoveForwardField, MoveBackwardField, MoveLeftField, MoveRightField, RunField, } from './index.js';
|
|
2
|
+
const DefaultDeadZonePx = 24;
|
|
3
|
+
const DefaultRunDistancePx = 46;
|
|
4
|
+
export class ScreenJoystickInput {
|
|
5
|
+
options;
|
|
6
|
+
root;
|
|
7
|
+
handle;
|
|
8
|
+
moveX = 0;
|
|
9
|
+
moveY = 0;
|
|
10
|
+
running = false;
|
|
11
|
+
joystickRadius = 56;
|
|
12
|
+
joyCenterX = 0;
|
|
13
|
+
joyCenterY = 0;
|
|
14
|
+
pointerId;
|
|
15
|
+
constructor(domElement, options = {}) {
|
|
16
|
+
this.options = options;
|
|
17
|
+
const parent = domElement.parentElement ?? domElement;
|
|
18
|
+
const joy = document.createElement('div');
|
|
19
|
+
joy.className = 'viverse-joystick mobile-only';
|
|
20
|
+
parent.appendChild(joy);
|
|
21
|
+
this.root = joy;
|
|
22
|
+
this.root.style.position = 'absolute';
|
|
23
|
+
this.root.style.bottom = '24px';
|
|
24
|
+
this.root.style.left = '24px';
|
|
25
|
+
this.root.style.width = '112px';
|
|
26
|
+
this.root.style.height = '112px';
|
|
27
|
+
this.root.style.borderRadius = '9999px';
|
|
28
|
+
this.root.style.background = 'rgba(255,255,255,0.2)';
|
|
29
|
+
this.root.style.pointerEvents = 'auto';
|
|
30
|
+
this.root.style.touchAction = 'none';
|
|
31
|
+
this.root.style.userSelect = 'none';
|
|
32
|
+
this.root.style.setProperty('-webkit-user-select', 'none');
|
|
33
|
+
this.root.style.setProperty('-webkit-touch-callout', 'none');
|
|
34
|
+
const handle = document.createElement('div');
|
|
35
|
+
handle.className = 'viverse-joystick-handle';
|
|
36
|
+
joy.appendChild(handle);
|
|
37
|
+
this.handle = handle;
|
|
38
|
+
this.handle.style.position = 'absolute';
|
|
39
|
+
this.handle.style.left = '50%';
|
|
40
|
+
this.handle.style.top = '50%';
|
|
41
|
+
this.handle.style.width = '56px';
|
|
42
|
+
this.handle.style.height = '56px';
|
|
43
|
+
this.handle.style.borderRadius = '9999px';
|
|
44
|
+
this.handle.style.background = 'rgba(0,0,0,0.3)';
|
|
45
|
+
this.handle.style.transform = 'translate(-50%,-50%)';
|
|
46
|
+
this.handle.style.willChange = 'transform';
|
|
47
|
+
this.handle.style.pointerEvents = 'none';
|
|
48
|
+
this.handle.style.touchAction = 'none';
|
|
49
|
+
this.handle.style.userSelect = 'none';
|
|
50
|
+
this.handle.style.setProperty('-webkit-user-select', 'none');
|
|
51
|
+
this.handle.style.setProperty('-webkit-touch-callout', 'none');
|
|
52
|
+
const onPointerDown = (e) => {
|
|
53
|
+
if (this.pointerId != null) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
e.stopPropagation();
|
|
58
|
+
joy.setPointerCapture(e.pointerId);
|
|
59
|
+
this.pointerId = e.pointerId;
|
|
60
|
+
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);
|
|
64
|
+
};
|
|
65
|
+
const onPointerMove = (e) => {
|
|
66
|
+
if (this.pointerId == null) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
e.stopPropagation();
|
|
71
|
+
this.updateHandle(e.clientX - this.joyCenterX, e.clientY - this.joyCenterY);
|
|
72
|
+
};
|
|
73
|
+
const onPointerEnd = (e) => {
|
|
74
|
+
if (this.pointerId != e.pointerId) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
this.pointerId = undefined;
|
|
78
|
+
joy.releasePointerCapture(e.pointerId);
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
this.resetHandle();
|
|
81
|
+
};
|
|
82
|
+
joy.addEventListener('pointerdown', onPointerDown);
|
|
83
|
+
joy.addEventListener('pointermove', onPointerMove);
|
|
84
|
+
joy.addEventListener('pointerup', onPointerEnd);
|
|
85
|
+
joy.addEventListener('pointercancel', onPointerEnd);
|
|
86
|
+
}
|
|
87
|
+
get(field) {
|
|
88
|
+
switch (field) {
|
|
89
|
+
case MoveForwardField:
|
|
90
|
+
return Math.max(0, this.moveY);
|
|
91
|
+
case MoveBackwardField:
|
|
92
|
+
return Math.max(0, -this.moveY);
|
|
93
|
+
case MoveLeftField:
|
|
94
|
+
return Math.max(0, -this.moveX);
|
|
95
|
+
case MoveRightField:
|
|
96
|
+
return Math.max(0, this.moveX);
|
|
97
|
+
case RunField:
|
|
98
|
+
return this.running;
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
dispose() {
|
|
103
|
+
this.root.remove();
|
|
104
|
+
}
|
|
105
|
+
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);
|
|
120
|
+
}
|
|
121
|
+
resetHandle() {
|
|
122
|
+
this.handle.style.transform = 'translate(-50%,-50%)';
|
|
123
|
+
this.moveX = 0;
|
|
124
|
+
this.moveY = 0;
|
|
125
|
+
this.running = false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Input, InputField } from './index.js';
|
|
2
|
+
export declare class ScreenJumpButtonInput implements Input {
|
|
3
|
+
readonly root: HTMLDivElement;
|
|
4
|
+
private lastJumpTime;
|
|
5
|
+
constructor(domElement: HTMLElement);
|
|
6
|
+
get<T>(field: InputField<T>): T | undefined;
|
|
7
|
+
dispose(): void;
|
|
8
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { LastTimeJumpPressedField } from './index.js';
|
|
2
|
+
export class ScreenJumpButtonInput {
|
|
3
|
+
root;
|
|
4
|
+
lastJumpTime = null;
|
|
5
|
+
constructor(domElement) {
|
|
6
|
+
const parent = domElement.parentElement ?? domElement;
|
|
7
|
+
const btn = document.createElement('div');
|
|
8
|
+
btn.className = 'viverse-button viverse-jump mobile-only';
|
|
9
|
+
parent.appendChild(btn);
|
|
10
|
+
this.root = btn;
|
|
11
|
+
this.root.style.position = 'absolute';
|
|
12
|
+
this.root.style.bottom = '32px';
|
|
13
|
+
this.root.style.right = '126px';
|
|
14
|
+
this.root.style.minWidth = '64px';
|
|
15
|
+
this.root.style.height = '64px';
|
|
16
|
+
this.root.style.borderRadius = '9999px';
|
|
17
|
+
this.root.style.pointerEvents = 'auto';
|
|
18
|
+
this.root.style.touchAction = 'none';
|
|
19
|
+
this.root.style.userSelect = 'none';
|
|
20
|
+
this.root.style.setProperty('-webkit-user-select', 'none');
|
|
21
|
+
this.root.style.background = 'rgba(255,255,255,0.3)';
|
|
22
|
+
this.root.style.backgroundImage =
|
|
23
|
+
'url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23444%22%20stroke-width=%222%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Cpolyline%20points=%2218%2015%2012%209%206%2015%22/%3E%3C/svg%3E")';
|
|
24
|
+
this.root.style.backgroundRepeat = 'no-repeat';
|
|
25
|
+
this.root.style.backgroundPosition = 'center';
|
|
26
|
+
this.root.style.backgroundSize = '50%';
|
|
27
|
+
const onPress = (e) => {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
e.stopPropagation();
|
|
30
|
+
this.lastJumpTime = performance.now() / 1000;
|
|
31
|
+
};
|
|
32
|
+
const stopPropagation = (e) => {
|
|
33
|
+
e.stopPropagation();
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
};
|
|
36
|
+
this.root.addEventListener('pointerdown', onPress);
|
|
37
|
+
this.root.addEventListener('pointermove', stopPropagation);
|
|
38
|
+
this.root.addEventListener('pointerup', stopPropagation);
|
|
39
|
+
}
|
|
40
|
+
get(field) {
|
|
41
|
+
if (field === LastTimeJumpPressedField) {
|
|
42
|
+
return this.lastJumpTime;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
dispose() {
|
|
47
|
+
this.root.remove();
|
|
48
|
+
}
|
|
49
|
+
}
|
package/dist/physics/index.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export type BvhCharacterPhysicsOptions = {
|
|
|
34
34
|
export declare class BvhCharacterPhysics {
|
|
35
35
|
private readonly character;
|
|
36
36
|
private readonly world;
|
|
37
|
-
private
|
|
37
|
+
private disposed;
|
|
38
38
|
private readonly stateVelocity;
|
|
39
39
|
readonly inputVelocity: Vector3;
|
|
40
40
|
private notGroundedSeconds;
|
|
@@ -49,7 +49,7 @@ export declare class BvhCharacterPhysics {
|
|
|
49
49
|
*/
|
|
50
50
|
update(fullDelta: number, options?: BvhCharacterPhysicsOptions): void;
|
|
51
51
|
private updateBoundingShapes;
|
|
52
|
-
|
|
52
|
+
dispose(): void;
|
|
53
53
|
shapecastCapsule(position: Vector3, maxGroundSlope: number, options: Exclude<BvhCharacterPhysicsOptions, boolean>): boolean;
|
|
54
54
|
}
|
|
55
55
|
export * from './world.js';
|
package/dist/physics/index.js
CHANGED
|
@@ -13,7 +13,7 @@ const YAxis = new Vector3(0, 1, 0);
|
|
|
13
13
|
export class BvhCharacterPhysics {
|
|
14
14
|
character;
|
|
15
15
|
world;
|
|
16
|
-
|
|
16
|
+
disposed = false;
|
|
17
17
|
stateVelocity = new Vector3();
|
|
18
18
|
inputVelocity = new Vector3();
|
|
19
19
|
notGroundedSeconds = 0;
|
|
@@ -40,7 +40,7 @@ export class BvhCharacterPhysics {
|
|
|
40
40
|
if (options === true) {
|
|
41
41
|
options = {};
|
|
42
42
|
}
|
|
43
|
-
if (this.
|
|
43
|
+
if (this.disposed) {
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
46
|
//at max catch up to 1 second of physics in one update call (running at less then 1fps is unplayable anyways)
|
|
@@ -101,8 +101,8 @@ export class BvhCharacterPhysics {
|
|
|
101
101
|
this.aabbox.min.addScalar(-this.radius);
|
|
102
102
|
this.aabbox.max.addScalar(this.radius);
|
|
103
103
|
}
|
|
104
|
-
|
|
105
|
-
this.
|
|
104
|
+
dispose() {
|
|
105
|
+
this.disposed = true;
|
|
106
106
|
}
|
|
107
107
|
shapecastCapsule(position, maxGroundSlope, options) {
|
|
108
108
|
this.updateBoundingShapes(options);
|
package/dist/physics/world.js
CHANGED
|
@@ -47,7 +47,7 @@ export class BvhPhysicsWorld {
|
|
|
47
47
|
throw new Error(`cannot add InstancedMesh with children`);
|
|
48
48
|
}
|
|
49
49
|
const bvh = computeBoundsTree.apply(object.geometry);
|
|
50
|
-
return new Array(object.instanceMatrix).fill(undefined).map((_, i) => ({
|
|
50
|
+
return new Array(object.instanceMatrix.count).fill(undefined).map((_, i) => ({
|
|
51
51
|
object,
|
|
52
52
|
bvh,
|
|
53
53
|
instanceIndex: i,
|
|
@@ -2,7 +2,7 @@ import { VRM } from '@pixiv/three-vrm';
|
|
|
2
2
|
import { AnimationAction, AnimationClip, AnimationMixer, Group, Object3D, Object3DEventMap, Quaternion } from 'three';
|
|
3
3
|
import { simpleCharacterAnimationNames, ModelAnimationOptions } from './animation/index.js';
|
|
4
4
|
import { SimpleCharacterCameraBehavior, SimpleCharacterCameraBehaviorOptions } from './camera.js';
|
|
5
|
-
import { Input, InputSystem } from './input/index.js';
|
|
5
|
+
import { Input, InputSystem, ScreenJoystickInputOptions, LocomotionKeyboardInputOptions, PointerCaptureInputOptions, PointerLockInputOptions } from './input/index.js';
|
|
6
6
|
import { CharacterModelOptions, loadCharacterModel } from './model/index.js';
|
|
7
7
|
import { BvhCharacterPhysicsOptions, BvhCharacterPhysics, BvhPhysicsWorld } from './physics/index.js';
|
|
8
8
|
export type SimpleCharacterMovementOptions = {
|
|
@@ -57,13 +57,12 @@ export type SimpleCharacterAnimationOptions = {
|
|
|
57
57
|
*/
|
|
58
58
|
crossFadeDuration?: number;
|
|
59
59
|
};
|
|
60
|
+
export type SimpleCharacterInputOptions = ScreenJoystickInputOptions & PointerCaptureInputOptions & PointerLockInputOptions & LocomotionKeyboardInputOptions;
|
|
60
61
|
export type SimpleCharacterOptions = {
|
|
61
|
-
/**
|
|
62
|
-
* @default [LocomotionKeyboardInput,PointerCaptureInput]
|
|
63
|
-
*/
|
|
64
62
|
readonly input?: ReadonlyArray<Input | {
|
|
65
63
|
new (domElement: HTMLElement): Input;
|
|
66
64
|
}> | InputSystem;
|
|
65
|
+
inputOptions?: SimpleCharacterInputOptions;
|
|
67
66
|
movement?: SimpleCharacterMovementOptions;
|
|
68
67
|
readonly model?: CharacterModelOptions;
|
|
69
68
|
physics?: BvhCharacterPhysicsOptions;
|
|
@@ -90,6 +89,7 @@ export declare function preloadSimpleCharacterAssets(options: Pick<SimpleCharact
|
|
|
90
89
|
export declare class SimpleCharacter extends Group<Object3DEventMap & {
|
|
91
90
|
loaded: {};
|
|
92
91
|
}> {
|
|
92
|
+
private readonly camera;
|
|
93
93
|
readonly options: SimpleCharacterOptions;
|
|
94
94
|
readonly cameraBehavior: SimpleCharacterCameraBehavior;
|
|
95
95
|
readonly physics: BvhCharacterPhysics;
|
|
@@ -98,7 +98,8 @@ export declare class SimpleCharacter extends Group<Object3DEventMap & {
|
|
|
98
98
|
actions?: Record<(typeof simpleCharacterAnimationNames)[number], AnimationAction> | undefined;
|
|
99
99
|
model?: Awaited<Exclude<ReturnType<typeof loadCharacterModel>, undefined>>;
|
|
100
100
|
private updateTimeline?;
|
|
101
|
-
constructor(camera: Object3D, world: BvhPhysicsWorld, domElement: HTMLElement, options?: SimpleCharacterOptions);
|
|
101
|
+
constructor(camera: Object3D | (() => Object3D), world: BvhPhysicsWorld, domElement: HTMLElement, options?: SimpleCharacterOptions);
|
|
102
|
+
getCamera(): Object3D<Object3DEventMap>;
|
|
102
103
|
private init;
|
|
103
104
|
update(delta: number): void;
|
|
104
105
|
dispose(): void;
|
package/dist/simple-character.js
CHANGED
|
@@ -3,9 +3,10 @@ import { action, animationFinished, start, timePassed, forever, parallel, graph,
|
|
|
3
3
|
import { AnimationMixer, Euler, Group, LoopOnce, Quaternion, Vector3, } from 'three';
|
|
4
4
|
import { simpleCharacterAnimationNames, getSimpleCharacterModelAnimationOptions as getSimpleCharacterModelAnimationOptions, loadCharacterModelAnimation as loadCharacterModelAnimation, } from './animation/index.js';
|
|
5
5
|
import { SimpleCharacterCameraBehavior } from './camera.js';
|
|
6
|
-
import { InputSystem, LocomotionKeyboardInput, MoveBackwardField, MoveForwardField, MoveLeftField, MoveRightField, PointerCaptureInput, RunField, LastTimeJumpPressedField, } from './input/index.js';
|
|
6
|
+
import { InputSystem, LocomotionKeyboardInput, MoveBackwardField, MoveForwardField, MoveLeftField, MoveRightField, PointerCaptureInput, RunField, LastTimeJumpPressedField, ScreenJoystickInput, ScreenJumpButtonInput, } from './input/index.js';
|
|
7
7
|
import { clearCharacterModelCache, loadCharacterModel } from './model/index.js';
|
|
8
8
|
import { BvhCharacterPhysics } from './physics/index.js';
|
|
9
|
+
import { extractProxy } from './utils.js';
|
|
9
10
|
const DefaultCrossFadeDuration = 0.1;
|
|
10
11
|
const DefaultJumDelay = 0.2;
|
|
11
12
|
//constants
|
|
@@ -35,7 +36,7 @@ export async function preloadSimpleCharacterAssets(options) {
|
|
|
35
36
|
}, {}),
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
|
-
async function* SimpleCharacterTimeline(
|
|
39
|
+
async function* SimpleCharacterTimeline(character) {
|
|
39
40
|
let lastJump = 0;
|
|
40
41
|
function shouldJump() {
|
|
41
42
|
let jumpOptions = character.options.movement?.jump;
|
|
@@ -55,6 +56,10 @@ async function* SimpleCharacterTimeline(camera, character) {
|
|
|
55
56
|
if (lastJump > lastTimePressed) {
|
|
56
57
|
return false;
|
|
57
58
|
}
|
|
59
|
+
//last jump must be more then 0.3 second ago, if not, we dont jump, this is to give the character time to get off the ground
|
|
60
|
+
if (lastJump > performance.now() / 1000 - 0.3) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
58
63
|
return performance.now() / 1000 - lastTimePressed < (jumpOptions?.bufferTime ?? 0.1);
|
|
59
64
|
}
|
|
60
65
|
function applyJumpForce() {
|
|
@@ -69,7 +74,7 @@ async function* SimpleCharacterTimeline(camera, character) {
|
|
|
69
74
|
// character movement
|
|
70
75
|
action({
|
|
71
76
|
update() {
|
|
72
|
-
cameraEuler.setFromQuaternion(
|
|
77
|
+
cameraEuler.setFromQuaternion(character.getCamera().getWorldQuaternion(cameraRotation), 'YXZ');
|
|
73
78
|
cameraEuler.x = 0;
|
|
74
79
|
cameraEuler.z = 0;
|
|
75
80
|
let inputSpeed = 0;
|
|
@@ -100,7 +105,7 @@ async function* SimpleCharacterTimeline(camera, character) {
|
|
|
100
105
|
const basedOn = character.options.animation?.yawRotationBasdOn ?? 'movement';
|
|
101
106
|
// compute goalTargetEuler
|
|
102
107
|
if (basedOn === 'camera') {
|
|
103
|
-
goalTargetEuler.setFromQuaternion(
|
|
108
|
+
goalTargetEuler.setFromQuaternion(character.getCamera().getWorldQuaternion(quaternion), 'YXZ');
|
|
104
109
|
}
|
|
105
110
|
else {
|
|
106
111
|
//don't rotate if not moving
|
|
@@ -269,6 +274,7 @@ async function* SimpleCharacterTimeline(camera, character) {
|
|
|
269
274
|
}));
|
|
270
275
|
}
|
|
271
276
|
export class SimpleCharacter extends Group {
|
|
277
|
+
camera;
|
|
272
278
|
options;
|
|
273
279
|
cameraBehavior;
|
|
274
280
|
physics;
|
|
@@ -281,20 +287,24 @@ export class SimpleCharacter extends Group {
|
|
|
281
287
|
updateTimeline;
|
|
282
288
|
constructor(camera, world, domElement, options = {}) {
|
|
283
289
|
super();
|
|
290
|
+
this.camera = camera;
|
|
284
291
|
this.options = options;
|
|
285
292
|
// input system
|
|
286
293
|
this.inputSystem =
|
|
287
294
|
options.input instanceof InputSystem
|
|
288
295
|
? options.input
|
|
289
|
-
: new InputSystem(domElement, options.input ?? [
|
|
296
|
+
: new InputSystem(domElement, options.input ?? [ScreenJoystickInput, ScreenJumpButtonInput, PointerCaptureInput, LocomotionKeyboardInput], extractProxy(options, 'inputOptions'));
|
|
290
297
|
options.physics ??= {};
|
|
291
298
|
// camera behavior
|
|
292
|
-
this.cameraBehavior = new SimpleCharacterCameraBehavior(camera, this, world.raycast.bind(world));
|
|
299
|
+
this.cameraBehavior = new SimpleCharacterCameraBehavior(typeof camera === 'function' ? camera : () => camera, this, world.raycast.bind(world));
|
|
293
300
|
// physics
|
|
294
301
|
this.physics = new BvhCharacterPhysics(this, world);
|
|
295
|
-
this.init(
|
|
302
|
+
this.init(options).catch(console.error);
|
|
303
|
+
}
|
|
304
|
+
getCamera() {
|
|
305
|
+
return typeof this.camera === 'function' ? this.camera() : this.camera;
|
|
296
306
|
}
|
|
297
|
-
async init(
|
|
307
|
+
async init(options) {
|
|
298
308
|
const { model, animations } = await preloadSimpleCharacterAssets(options);
|
|
299
309
|
this.model = model;
|
|
300
310
|
if (model != null && animations != null) {
|
|
@@ -310,7 +320,7 @@ export class SimpleCharacter extends Group {
|
|
|
310
320
|
this.actions.jumpForward.loop = LoopOnce;
|
|
311
321
|
this.actions.jumpForward.clampWhenFinished = true;
|
|
312
322
|
}
|
|
313
|
-
this.updateTimeline = start(SimpleCharacterTimeline(
|
|
323
|
+
this.updateTimeline = start(SimpleCharacterTimeline(this));
|
|
314
324
|
this.dispatchEvent({ type: 'loaded' });
|
|
315
325
|
}
|
|
316
326
|
update(delta) {
|
|
@@ -325,6 +335,8 @@ export class SimpleCharacter extends Group {
|
|
|
325
335
|
dispose() {
|
|
326
336
|
this.parent?.remove(this);
|
|
327
337
|
this.model?.scene.dispatchEvent({ type: 'dispose' });
|
|
338
|
+
this.inputSystem.dispose();
|
|
339
|
+
this.physics.dispose();
|
|
328
340
|
VRMUtils.deepDispose(this);
|
|
329
341
|
}
|
|
330
342
|
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
export declare function cached<D extends ReadonlyArray<unknown>, T>(fn: (...deps: D) => Promise<T>, dependencies: D): Promise<T>;
|
|
2
2
|
export declare function clearCache(fn: Function, dependencies: Array<unknown>): void;
|
|
3
|
+
export declare function extractProxy<K extends string, T extends {
|
|
4
|
+
[key in K]?: {};
|
|
5
|
+
}>(value: T, key: K): Partial<Exclude<T[K], undefined>>;
|
|
6
|
+
export declare function getIsMobileMediaQuery(): MediaQueryList | undefined;
|
|
7
|
+
export declare function isMobile(): boolean;
|
package/dist/utils.js
CHANGED
|
@@ -32,3 +32,17 @@ export function clearCache(fn, dependencies) {
|
|
|
32
32
|
}
|
|
33
33
|
cache.splice(index, 1);
|
|
34
34
|
}
|
|
35
|
+
export function extractProxy(value, key) {
|
|
36
|
+
return new Proxy({}, {
|
|
37
|
+
get: (_, p) => value[key]?.[p],
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function getIsMobileMediaQuery() {
|
|
41
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
return window.matchMedia('(hover: none) and (pointer: coarse)');
|
|
45
|
+
}
|
|
46
|
+
export function isMobile() {
|
|
47
|
+
return getIsMobileMediaQuery()?.matches ?? false;
|
|
48
|
+
}
|