@pmndrs/viverse 0.1.17 → 0.1.19
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/fbx.d.ts +4 -0
- package/dist/animation/{mixamo.js → fbx.js} +2 -3
- package/dist/animation/gltf.d.ts +2 -2
- package/dist/animation/gltf.js +2 -2
- package/dist/animation/index.d.ts +4 -2
- package/dist/animation/index.js +83 -42
- 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/mixamo.d.ts +0 -3
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { AnimationClip } from 'three';
|
|
2
|
+
import { loadCharacterModel } from '../model/index.js';
|
|
3
|
+
import type { VRMHumanBoneName } from '@pixiv/three-vrm';
|
|
4
|
+
export declare function loadVrmModelFbxAnimations(model: Exclude<Awaited<ReturnType<typeof loadCharacterModel>>, undefined>, url: string, removeXZMovement: boolean, boneMap?: Record<string, VRMHumanBoneName>): Promise<Array<AnimationClip>>;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { FBXLoader } from 'three/examples/jsm/Addons.js';
|
|
2
2
|
import { fixModelAnimationClip } from './index.js';
|
|
3
|
-
import mixamoBoneMap from './mixamo-bone-map.json';
|
|
4
3
|
const loader = new FBXLoader();
|
|
5
|
-
export async function
|
|
4
|
+
export async function loadVrmModelFbxAnimations(model, url, removeXZMovement, boneMap) {
|
|
6
5
|
const clipScene = await loader.loadAsync(url);
|
|
7
|
-
clipScene.animations.forEach((clip) => fixModelAnimationClip(model, clip, clipScene, removeXZMovement,
|
|
6
|
+
clipScene.animations.forEach((clip) => fixModelAnimationClip(model, clip, clipScene, removeXZMovement, boneMap));
|
|
8
7
|
return clipScene.animations;
|
|
9
8
|
}
|
package/dist/animation/gltf.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { AnimationClip } from 'three';
|
|
2
|
-
import { loadCharacterModel } from '../model/index.js';
|
|
3
|
-
export declare function loadVrmModelGltfAnimations(model: Exclude<Awaited<ReturnType<typeof loadCharacterModel>>, undefined>, url: string, removeXZMovement: boolean): Promise<Array<AnimationClip>>;
|
|
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>>;
|
package/dist/animation/gltf.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { GLTFLoader } from 'three/examples/jsm/Addons.js';
|
|
2
2
|
import { fixModelAnimationClip } from './index.js';
|
|
3
3
|
const loader = new GLTFLoader();
|
|
4
|
-
export async function loadVrmModelGltfAnimations(model, url, removeXZMovement) {
|
|
4
|
+
export async function loadVrmModelGltfAnimations(model, url, removeXZMovement, boneMap) {
|
|
5
5
|
const { animations, scene: clipScene } = await loader.loadAsync(url);
|
|
6
|
-
animations.forEach((clip) => fixModelAnimationClip(model, clip, clipScene, removeXZMovement));
|
|
6
|
+
animations.forEach((clip) => fixModelAnimationClip(model, clip, clipScene, removeXZMovement, boneMap));
|
|
7
7
|
return animations;
|
|
8
8
|
}
|
|
@@ -3,17 +3,18 @@ import { AnimationClip, Object3D } from 'three';
|
|
|
3
3
|
import { loadCharacterModel } from '../model/index.js';
|
|
4
4
|
export declare function fixModelAnimationClip(model: Exclude<Awaited<ReturnType<typeof loadCharacterModel>>, undefined>, clip: AnimationClip, clipScene: Object3D, removeXZMovement: boolean, boneMap?: Record<string, VRMHumanBoneName>): void;
|
|
5
5
|
export * from './gltf.js';
|
|
6
|
-
export * from './
|
|
6
|
+
export * from './fbx.js';
|
|
7
7
|
export * from './vrma.js';
|
|
8
8
|
export * from './utils.js';
|
|
9
9
|
export type ModelAnimationOptions = {
|
|
10
|
-
type: 'mixamo' | 'gltf' | 'vrma';
|
|
10
|
+
type: 'mixamo' | 'gltf' | 'vrma' | 'fbx';
|
|
11
11
|
url: string;
|
|
12
12
|
removeXZMovement?: boolean;
|
|
13
13
|
trimTime?: {
|
|
14
14
|
start?: number;
|
|
15
15
|
end?: number;
|
|
16
16
|
};
|
|
17
|
+
boneMap?: Record<string, VRMHumanBoneName>;
|
|
17
18
|
scaleTime?: number;
|
|
18
19
|
};
|
|
19
20
|
export declare function loadCharacterModelAnimation(model: Exclude<Awaited<ReturnType<typeof loadCharacterModel>>, undefined>, options: ModelAnimationOptions): Promise<AnimationClip>;
|
|
@@ -28,3 +29,4 @@ declare const simpleCharacterAnimationUrls: {
|
|
|
28
29
|
};
|
|
29
30
|
export declare const simpleCharacterAnimationNames: Array<keyof typeof simpleCharacterAnimationUrls>;
|
|
30
31
|
export declare function getSimpleCharacterModelAnimationOptions(animationName: keyof typeof simpleCharacterAnimationUrls): Promise<ModelAnimationOptions>;
|
|
32
|
+
export declare const mixamoBoneMap: Record<string, VRMHumanBoneName>;
|
package/dist/animation/index.js
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
import { VRM } from '@pixiv/three-vrm';
|
|
2
2
|
import { Euler, Quaternion, QuaternionKeyframeTrack, Vector3, VectorKeyframeTrack, } from 'three';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { loadVrmModelFbxAnimations } from './fbx.js';
|
|
4
|
+
import { loadVrmModelGltfAnimations } from './gltf.js';
|
|
5
5
|
import { scaleAnimationClipTime, trimAnimationClip } from './utils.js';
|
|
6
6
|
import { loadVrmModelVrmaAnimations } from './vrma.js';
|
|
7
7
|
import { cached } from '../utils.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
const
|
|
8
|
+
import _mixamoBoneMap from './mixamo-bone-map.json';
|
|
9
|
+
//helper variables for the quaternion retargeting
|
|
10
|
+
const baseThisLocalRestRotation_inverse = new Quaternion();
|
|
11
|
+
const baseThisLocalCurrentRotation = new Quaternion();
|
|
12
|
+
const baseParentWorldRestRotation = new Quaternion();
|
|
13
|
+
const baseParentWorldRestRotation_inverse = new Quaternion();
|
|
14
|
+
const targetParentWorldRestRotation = new Quaternion();
|
|
15
|
+
const targetParentWorldRestRotation_inverse = new Quaternion();
|
|
16
|
+
const targetThisLocalRestRotation = new Quaternion();
|
|
17
|
+
const targetThisLocalCurrentRotation = new Quaternion();
|
|
18
|
+
//helper variables for the position retargeting
|
|
19
|
+
const position = new Vector3();
|
|
12
20
|
const nonVrmRotationOffset = new Quaternion().setFromEuler(new Euler(0, Math.PI, 0));
|
|
21
|
+
//TODO: currently assumes the model is not yet transformed - loaded for the first time
|
|
13
22
|
export function fixModelAnimationClip(model, clip, clipScene, removeXZMovement, boneMap) {
|
|
14
23
|
const hipsBoneName = boneMap == null ? 'hips' : Object.entries(boneMap).find(([, vrmBoneName]) => vrmBoneName === 'hips')?.[0];
|
|
15
24
|
if (hipsBoneName == null) {
|
|
16
25
|
throw new Error('Failed to determine hips bone name for VRM animation. Please check the bone map or animation file.');
|
|
17
26
|
}
|
|
18
27
|
const clipSceneHips = clipScene.getObjectByName(hipsBoneName);
|
|
28
|
+
clipSceneHips?.parent?.updateMatrixWorld();
|
|
19
29
|
const vrmHipsPosition = model instanceof VRM
|
|
20
30
|
? model.humanoid.normalizedRestPose.hips?.position
|
|
21
31
|
: model.scene.getObjectByName('hips')?.position.toArray();
|
|
@@ -23,82 +33,111 @@ export function fixModelAnimationClip(model, clip, clipScene, removeXZMovement,
|
|
|
23
33
|
throw new Error('Failed to load VRM animation: missing animation hips object or VRM hips position.');
|
|
24
34
|
}
|
|
25
35
|
// Adjust with reference to hips height.
|
|
26
|
-
const motionHipsHeight = clipSceneHips.position.y;
|
|
36
|
+
const motionHipsHeight = clipSceneHips.getWorldPosition(position).y;
|
|
27
37
|
const [_, vrmHipsHeight] = vrmHipsPosition;
|
|
28
|
-
const
|
|
38
|
+
const positionScale = vrmHipsHeight / motionHipsHeight;
|
|
29
39
|
for (const track of clip.tracks) {
|
|
30
40
|
// Convert each tracks for VRM use, and push to `tracks`
|
|
31
41
|
const [boneName, propertyName] = track.name.split('.');
|
|
32
42
|
const vrmBoneName = boneMap?.[boneName] ?? boneName;
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
const targetBone = model instanceof VRM
|
|
44
|
+
? model.humanoid.getNormalizedBoneNode(vrmBoneName)
|
|
45
|
+
: model.scene.getObjectByName(vrmBoneName);
|
|
46
|
+
if (targetBone == null) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const vrmNodeName = model instanceof VRM ? targetBone.name : vrmBoneName;
|
|
50
|
+
const baseBone = clipScene.getObjectByName(boneName);
|
|
51
|
+
if (vrmNodeName == null || baseBone == null) {
|
|
36
52
|
continue;
|
|
37
53
|
}
|
|
38
54
|
if (track instanceof QuaternionKeyframeTrack) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
// Store rotations of rest-pose.
|
|
56
|
+
baseThisLocalRestRotation_inverse.copy(baseBone.quaternion).invert();
|
|
57
|
+
if (baseBone.parent != null) {
|
|
58
|
+
baseBone.parent.getWorldQuaternion(baseParentWorldRestRotation);
|
|
59
|
+
baseParentWorldRestRotation_inverse.copy(baseParentWorldRestRotation).invert();
|
|
42
60
|
}
|
|
43
61
|
else {
|
|
44
|
-
|
|
45
|
-
|
|
62
|
+
baseParentWorldRestRotation.identity();
|
|
63
|
+
baseParentWorldRestRotation_inverse.identity();
|
|
64
|
+
}
|
|
65
|
+
targetThisLocalRestRotation.copy(targetBone.quaternion);
|
|
66
|
+
if (targetBone.parent != null) {
|
|
67
|
+
targetBone.parent.getWorldQuaternion(targetParentWorldRestRotation);
|
|
68
|
+
targetParentWorldRestRotation_inverse.copy(targetParentWorldRestRotation).invert();
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
targetParentWorldRestRotation.identity();
|
|
72
|
+
targetParentWorldRestRotation_inverse.identity();
|
|
46
73
|
}
|
|
47
|
-
// Store rotations of rest-pose.
|
|
48
74
|
for (let i = 0; i < track.values.length; i += 4) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
baseThisLocalCurrentRotation.fromArray(track.values, i);
|
|
76
|
+
targetThisLocalCurrentRotation
|
|
77
|
+
.copy(targetParentWorldRestRotation_inverse)
|
|
78
|
+
.multiply(baseParentWorldRestRotation)
|
|
79
|
+
.multiply(baseThisLocalCurrentRotation)
|
|
80
|
+
.multiply(baseThisLocalRestRotation_inverse)
|
|
81
|
+
.multiply(baseParentWorldRestRotation_inverse)
|
|
82
|
+
.multiply(targetParentWorldRestRotation)
|
|
83
|
+
.multiply(targetThisLocalRestRotation);
|
|
84
|
+
if (model instanceof VRM && model.meta.metaVersion === '0') {
|
|
85
|
+
targetThisLocalCurrentRotation.x *= -1;
|
|
86
|
+
targetThisLocalCurrentRotation.z *= -1;
|
|
56
87
|
}
|
|
57
|
-
if (vrmBoneName === '
|
|
58
|
-
|
|
88
|
+
if (!(model instanceof VRM) && vrmBoneName === 'hips') {
|
|
89
|
+
targetThisLocalCurrentRotation.premultiply(nonVrmRotationOffset);
|
|
59
90
|
}
|
|
60
|
-
|
|
61
|
-
//TODO
|
|
62
|
-
}
|
|
63
|
-
quaternion.toArray(track.values, i);
|
|
91
|
+
targetThisLocalCurrentRotation.toArray(track.values, i);
|
|
64
92
|
}
|
|
65
93
|
track.name = `${vrmNodeName}.${propertyName}`;
|
|
66
94
|
}
|
|
67
95
|
else if (track instanceof VectorKeyframeTrack) {
|
|
68
96
|
track.name = `${vrmNodeName}.${propertyName}`;
|
|
69
|
-
if (propertyName
|
|
97
|
+
if (propertyName != 'position') {
|
|
70
98
|
continue;
|
|
71
99
|
}
|
|
72
100
|
for (let i = 0; i < track.values.length; i += 3) {
|
|
73
|
-
|
|
74
|
-
|
|
101
|
+
position.fromArray(track.values, i);
|
|
102
|
+
if (clipSceneHips.parent != null) {
|
|
103
|
+
if (vrmBoneName === 'hips') {
|
|
104
|
+
position.applyMatrix4(clipSceneHips.parent.matrixWorld);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
position.multiplyScalar(clipSceneHips.parent.matrixWorld.getMaxScaleOnAxis());
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
position.multiplyScalar(positionScale);
|
|
75
111
|
if (model instanceof VRM) {
|
|
76
112
|
if (model.meta.metaVersion === '0') {
|
|
77
|
-
|
|
78
|
-
|
|
113
|
+
position.negate();
|
|
114
|
+
position.y *= -1;
|
|
79
115
|
}
|
|
80
116
|
}
|
|
81
117
|
if (vrmBoneName === 'hips' && removeXZMovement) {
|
|
82
|
-
|
|
83
|
-
|
|
118
|
+
position.x = 0;
|
|
119
|
+
position.z = 0;
|
|
84
120
|
}
|
|
85
|
-
|
|
121
|
+
position.toArray(track.values, i);
|
|
86
122
|
}
|
|
87
123
|
}
|
|
88
124
|
}
|
|
89
125
|
}
|
|
90
126
|
export * from './gltf.js';
|
|
91
|
-
export * from './
|
|
127
|
+
export * from './fbx.js';
|
|
92
128
|
export * from './vrma.js';
|
|
93
129
|
export * from './utils.js';
|
|
94
|
-
async function uncachedLoadModelAnimation(model, type, url, removeXZMovement, trimStartTime, trimEndTime, scaleTime) {
|
|
130
|
+
async function uncachedLoadModelAnimation(model, type, url, removeXZMovement, trimStartTime, trimEndTime, boneMap, scaleTime) {
|
|
95
131
|
let clips;
|
|
96
132
|
switch (type) {
|
|
97
133
|
case 'gltf':
|
|
98
|
-
clips = await
|
|
134
|
+
clips = await loadVrmModelGltfAnimations(model, url, removeXZMovement, boneMap);
|
|
135
|
+
break;
|
|
136
|
+
case 'fbx':
|
|
137
|
+
clips = await loadVrmModelFbxAnimations(model, url, removeXZMovement, boneMap);
|
|
99
138
|
break;
|
|
100
139
|
case 'mixamo':
|
|
101
|
-
clips = await
|
|
140
|
+
clips = await loadVrmModelFbxAnimations(model, url, removeXZMovement, boneMap ?? mixamoBoneMap);
|
|
102
141
|
break;
|
|
103
142
|
case 'vrma':
|
|
104
143
|
if (!(model instanceof VRM)) {
|
|
@@ -127,6 +166,7 @@ export function loadCharacterModelAnimation(model, options) {
|
|
|
127
166
|
options.removeXZMovement ?? false,
|
|
128
167
|
options.trimTime?.start,
|
|
129
168
|
options.trimTime?.end,
|
|
169
|
+
options.boneMap,
|
|
130
170
|
options.scaleTime,
|
|
131
171
|
]);
|
|
132
172
|
}
|
|
@@ -152,3 +192,4 @@ export async function getSimpleCharacterModelAnimationOptions(animationName) {
|
|
|
152
192
|
url: (await simpleCharacterAnimationUrls[animationName]()).url,
|
|
153
193
|
};
|
|
154
194
|
}
|
|
195
|
+
export const mixamoBoneMap = _mixamoBoneMap;
|
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import { AnimationClip } from 'three';
|
|
2
|
-
import { loadCharacterModel } from '../model/index.js';
|
|
3
|
-
export declare function loadVrmModelMixamoAnimations(model: Exclude<Awaited<ReturnType<typeof loadCharacterModel>>, undefined>, url: string, removeXZMovement: boolean): Promise<Array<AnimationClip>>;
|