@pmndrs/viverse 0.1.2
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/LICENSE +34 -0
- package/README.md +41 -0
- package/dist/animation/gltf.d.ts +3 -0
- package/dist/animation/gltf.js +8 -0
- package/dist/animation/index.d.ts +30 -0
- package/dist/animation/index.js +150 -0
- package/dist/animation/mixamo-bone-map.json +54 -0
- package/dist/animation/mixamo.d.ts +3 -0
- package/dist/animation/mixamo.js +9 -0
- package/dist/animation/utils.d.ts +3 -0
- package/dist/animation/utils.js +28 -0
- package/dist/animation/vrma.d.ts +3 -0
- package/dist/animation/vrma.js +8 -0
- package/dist/assets/idle.d.ts +1 -0
- package/dist/assets/idle.js +1 -0
- package/dist/assets/jump-down.d.ts +1 -0
- package/dist/assets/jump-down.js +1 -0
- package/dist/assets/jump-forward.d.ts +1 -0
- package/dist/assets/jump-forward.js +1 -0
- package/dist/assets/jump-loop.d.ts +1 -0
- package/dist/assets/jump-loop.js +1 -0
- package/dist/assets/jump-up.d.ts +1 -0
- package/dist/assets/jump-up.js +1 -0
- package/dist/assets/mannequin.d.ts +1 -0
- package/dist/assets/mannequin.js +1 -0
- package/dist/assets/prototype-texture.d.ts +1 -0
- package/dist/assets/prototype-texture.js +1 -0
- package/dist/assets/run.d.ts +1 -0
- package/dist/assets/run.js +1 -0
- package/dist/assets/walk.d.ts +1 -0
- package/dist/assets/walk.js +1 -0
- package/dist/camera.d.ts +79 -0
- package/dist/camera.js +139 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/input/index.d.ts +30 -0
- package/dist/input/index.js +64 -0
- package/dist/input/keyboard.d.ts +8 -0
- package/dist/input/keyboard.js +77 -0
- package/dist/input/pointer-capture.d.ts +14 -0
- package/dist/input/pointer-capture.js +59 -0
- package/dist/input/pointer-lock.d.ts +13 -0
- package/dist/input/pointer-lock.js +51 -0
- package/dist/material.d.ts +6 -0
- package/dist/material.js +18 -0
- package/dist/model/gltf.d.ts +8 -0
- package/dist/model/gltf.js +7 -0
- package/dist/model/index.d.ts +32 -0
- package/dist/model/index.js +57 -0
- package/dist/model/vrm.d.ts +9 -0
- package/dist/model/vrm.js +19 -0
- package/dist/physics/index.d.ts +50 -0
- package/dist/physics/index.js +127 -0
- package/dist/physics/world.d.ts +9 -0
- package/dist/physics/world.js +27 -0
- package/dist/simple-character.d.ts +105 -0
- package/dist/simple-character.js +329 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +34 -0
- package/package.json +47 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Input, InputField } from './index.js';
|
|
2
|
+
/**
|
|
3
|
+
* @requires to manually execute `domElement.setPointerCapture(pointerId)` on pointerdown
|
|
4
|
+
*/
|
|
5
|
+
export declare class PointerCaptureInput implements Input {
|
|
6
|
+
private readonly domElement;
|
|
7
|
+
private readonly abortController;
|
|
8
|
+
private deltaZoom;
|
|
9
|
+
private deltaYaw;
|
|
10
|
+
private deltaPitch;
|
|
11
|
+
constructor(domElement: HTMLElement);
|
|
12
|
+
get<T>(field: InputField<T>): T | undefined;
|
|
13
|
+
destroy(): void;
|
|
14
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { DeltaPitchField, DeltaYawField, DeltaZoomField } from './index.js';
|
|
2
|
+
/**
|
|
3
|
+
* @requires to manually execute `domElement.setPointerCapture(pointerId)` on pointerdown
|
|
4
|
+
*/
|
|
5
|
+
export class PointerCaptureInput {
|
|
6
|
+
domElement;
|
|
7
|
+
abortController = new AbortController();
|
|
8
|
+
deltaZoom = 0;
|
|
9
|
+
deltaYaw = 0;
|
|
10
|
+
deltaPitch = 0;
|
|
11
|
+
constructor(domElement) {
|
|
12
|
+
this.domElement = domElement;
|
|
13
|
+
domElement.addEventListener('pointerdown', (event) => this.domElement.setPointerCapture(event.pointerId), {
|
|
14
|
+
signal: this.abortController.signal,
|
|
15
|
+
});
|
|
16
|
+
domElement.addEventListener('pointermove', (event) => {
|
|
17
|
+
if (!this.domElement.hasPointerCapture(event.pointerId)) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
this.deltaYaw -= (0.4 * event.movementX) / window.innerHeight;
|
|
21
|
+
this.deltaPitch -= (0.4 * event.movementY) / window.innerHeight;
|
|
22
|
+
}, {
|
|
23
|
+
signal: this.abortController.signal,
|
|
24
|
+
});
|
|
25
|
+
domElement.addEventListener('pointerup', (event) => this.domElement.releasePointerCapture(event.pointerId), {
|
|
26
|
+
signal: this.abortController.signal,
|
|
27
|
+
});
|
|
28
|
+
domElement.addEventListener('pointercancel', (event) => this.domElement.releasePointerCapture(event.pointerId), {
|
|
29
|
+
signal: this.abortController.signal,
|
|
30
|
+
});
|
|
31
|
+
domElement.addEventListener('wheel', (event) => {
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
this.deltaZoom += event.deltaY * 0.0001;
|
|
34
|
+
}, {
|
|
35
|
+
signal: this.abortController.signal,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
get(field) {
|
|
39
|
+
let result;
|
|
40
|
+
switch (field) {
|
|
41
|
+
case DeltaPitchField:
|
|
42
|
+
result = this.deltaPitch;
|
|
43
|
+
this.deltaPitch = 0;
|
|
44
|
+
break;
|
|
45
|
+
case DeltaYawField:
|
|
46
|
+
result = this.deltaYaw;
|
|
47
|
+
this.deltaYaw = 0;
|
|
48
|
+
break;
|
|
49
|
+
case DeltaZoomField:
|
|
50
|
+
result = this.deltaZoom;
|
|
51
|
+
this.deltaZoom = 0;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
destroy() {
|
|
57
|
+
this.abortController.abort();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Input, InputField } from './index.js';
|
|
2
|
+
/**
|
|
3
|
+
* @requires to manually execute `domElement.requestPointerLock()`
|
|
4
|
+
*/
|
|
5
|
+
export declare class PointerLockInput implements Input {
|
|
6
|
+
private readonly abortController;
|
|
7
|
+
private deltaZoom;
|
|
8
|
+
private deltaYaw;
|
|
9
|
+
private deltaPitch;
|
|
10
|
+
constructor(domElement: HTMLElement);
|
|
11
|
+
get<T>(field: InputField<T>): T | undefined;
|
|
12
|
+
destroy(): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { DeltaPitchField, DeltaYawField, DeltaZoomField } from './index.js';
|
|
2
|
+
/**
|
|
3
|
+
* @requires to manually execute `domElement.requestPointerLock()`
|
|
4
|
+
*/
|
|
5
|
+
export class PointerLockInput {
|
|
6
|
+
abortController = new AbortController();
|
|
7
|
+
deltaZoom = 0;
|
|
8
|
+
deltaYaw = 0;
|
|
9
|
+
deltaPitch = 0;
|
|
10
|
+
constructor(domElement) {
|
|
11
|
+
domElement.addEventListener('pointermove', (event) => {
|
|
12
|
+
if (document.pointerLockElement != domElement) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
this.deltaYaw -= (0.4 * event.movementX) / window.innerHeight;
|
|
16
|
+
this.deltaPitch -= (0.4 * event.movementY) / window.innerHeight;
|
|
17
|
+
}, {
|
|
18
|
+
signal: this.abortController.signal,
|
|
19
|
+
});
|
|
20
|
+
domElement.addEventListener('wheel', (event) => {
|
|
21
|
+
if (document.pointerLockElement != domElement) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
this.deltaZoom += event.deltaY * 0.0001;
|
|
25
|
+
event.preventDefault();
|
|
26
|
+
}, {
|
|
27
|
+
signal: this.abortController.signal,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
get(field) {
|
|
31
|
+
let result;
|
|
32
|
+
switch (field) {
|
|
33
|
+
case DeltaPitchField:
|
|
34
|
+
result = this.deltaPitch;
|
|
35
|
+
this.deltaPitch = 0;
|
|
36
|
+
break;
|
|
37
|
+
case DeltaYawField:
|
|
38
|
+
result = this.deltaYaw;
|
|
39
|
+
this.deltaYaw = 0;
|
|
40
|
+
break;
|
|
41
|
+
case DeltaZoomField:
|
|
42
|
+
result = this.deltaZoom;
|
|
43
|
+
this.deltaZoom = 0;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
destroy() {
|
|
49
|
+
this.abortController.abort();
|
|
50
|
+
}
|
|
51
|
+
}
|
package/dist/material.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { MeshPhongMaterial, RepeatWrapping, SRGBColorSpace, TextureLoader, Vector2 } from 'three';
|
|
2
|
+
const loader = new TextureLoader();
|
|
3
|
+
export class PrototypeMaterial extends MeshPhongMaterial {
|
|
4
|
+
repeat = new Vector2(2, 2);
|
|
5
|
+
constructor() {
|
|
6
|
+
super({ toneMapped: false, shininess: 0 });
|
|
7
|
+
this.init().catch(console.error);
|
|
8
|
+
}
|
|
9
|
+
async init() {
|
|
10
|
+
const texture = await loader.loadAsync((await import('./assets/prototype-texture.js')).url);
|
|
11
|
+
this.map = texture;
|
|
12
|
+
texture.colorSpace = SRGBColorSpace;
|
|
13
|
+
this.map.wrapS = RepeatWrapping;
|
|
14
|
+
this.map.wrapT = RepeatWrapping;
|
|
15
|
+
this.needsUpdate = true;
|
|
16
|
+
this.map.repeat = this.repeat;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Object3D, Object3DEventMap } from 'three';
|
|
2
|
+
import { GLTF, GLTFLoader } from 'three/examples/jsm/Addons.js';
|
|
3
|
+
export declare const gltfLoader: GLTFLoader;
|
|
4
|
+
export declare function loadGltfCharacterModel(url: string): Promise<GLTF & {
|
|
5
|
+
scene: Object3D<Object3DEventMap & {
|
|
6
|
+
dispose: {};
|
|
7
|
+
}>;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { GLTFLoader } from 'three/examples/jsm/Addons.js';
|
|
2
|
+
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
|
|
3
|
+
export const gltfLoader = new GLTFLoader();
|
|
4
|
+
gltfLoader.setMeshoptDecoder(MeshoptDecoder);
|
|
5
|
+
export async function loadGltfCharacterModel(url) {
|
|
6
|
+
return (await gltfLoader.loadAsync(url));
|
|
7
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Quaternion } from 'three';
|
|
2
|
+
export { VRMHumanBoneName } from '@pixiv/three-vrm';
|
|
3
|
+
export * from './vrm.js';
|
|
4
|
+
export type CharacterModelOptions = {
|
|
5
|
+
readonly type?: 'vrm' | 'gltf';
|
|
6
|
+
readonly url?: string;
|
|
7
|
+
/**
|
|
8
|
+
* allows to apply an rotation offset when placing objects as children of the character's bones
|
|
9
|
+
* @default undefined
|
|
10
|
+
*/
|
|
11
|
+
readonly boneRotationOffset?: Quaternion;
|
|
12
|
+
/**
|
|
13
|
+
* @default true
|
|
14
|
+
*/
|
|
15
|
+
readonly castShadow?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* @default true
|
|
18
|
+
*/
|
|
19
|
+
readonly receiveShadow?: boolean;
|
|
20
|
+
} | boolean;
|
|
21
|
+
export declare function clearCharacterModelCache(options?: CharacterModelOptions): void;
|
|
22
|
+
export declare function loadCharacterModel(options?: CharacterModelOptions): Promise<((import("@pixiv/three-vrm").VRM & {
|
|
23
|
+
scene: import("three").Object3D<import("three").Object3DEventMap & {
|
|
24
|
+
dispose: {};
|
|
25
|
+
}>;
|
|
26
|
+
}) | (import("three/examples/jsm/Addons.js").GLTF & {
|
|
27
|
+
scene: import("three").Object3D<import("three").Object3DEventMap & {
|
|
28
|
+
dispose: {};
|
|
29
|
+
}>;
|
|
30
|
+
})) & {
|
|
31
|
+
boneRotationOffset?: Quaternion;
|
|
32
|
+
}> | undefined;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Euler, Quaternion } from 'three';
|
|
2
|
+
import { loadVrmCharacterModel } from './vrm.js';
|
|
3
|
+
import { cached, clearCache } from '../utils.js';
|
|
4
|
+
import { loadGltfCharacterModel } from './gltf.js';
|
|
5
|
+
export { VRMHumanBoneName } from '@pixiv/three-vrm';
|
|
6
|
+
export * from './vrm.js';
|
|
7
|
+
async function uncachedLoadCharacterModel(type, url, boneRotationOffset, castShadow = true, receiveShadow = true) {
|
|
8
|
+
let result;
|
|
9
|
+
if (type == null || url == null) {
|
|
10
|
+
//prepare loading the default model
|
|
11
|
+
type = 'gltf';
|
|
12
|
+
url = (await import('../assets/mannequin.js')).url;
|
|
13
|
+
boneRotationOffset = new Quaternion().setFromEuler(new Euler(Math.PI, 0, Math.PI / 2, 'ZYX'));
|
|
14
|
+
}
|
|
15
|
+
switch (type) {
|
|
16
|
+
case 'vrm':
|
|
17
|
+
result = await loadVrmCharacterModel(url);
|
|
18
|
+
break;
|
|
19
|
+
case 'gltf':
|
|
20
|
+
result = await loadGltfCharacterModel(url);
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
result.boneRotationOffset = boneRotationOffset;
|
|
24
|
+
result.scene.traverse((obj) => {
|
|
25
|
+
obj.frustumCulled = false;
|
|
26
|
+
if (castShadow) {
|
|
27
|
+
obj.castShadow = true;
|
|
28
|
+
}
|
|
29
|
+
if (receiveShadow) {
|
|
30
|
+
obj.receiveShadow = true;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
function getCharacterModelDependencies(options = true) {
|
|
36
|
+
if (options === false) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
if (options === true) {
|
|
40
|
+
return [undefined, undefined, undefined, undefined, undefined];
|
|
41
|
+
}
|
|
42
|
+
return [options.type, options.url, options.boneRotationOffset, options.castShadow, options.receiveShadow];
|
|
43
|
+
}
|
|
44
|
+
export function clearCharacterModelCache(options) {
|
|
45
|
+
const dependencies = getCharacterModelDependencies(options);
|
|
46
|
+
if (dependencies == null) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
clearCache(uncachedLoadCharacterModel, dependencies);
|
|
50
|
+
}
|
|
51
|
+
export function loadCharacterModel(options) {
|
|
52
|
+
const dependencies = getCharacterModelDependencies(options);
|
|
53
|
+
if (dependencies == null) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return cached(uncachedLoadCharacterModel, dependencies);
|
|
57
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { VRM } from '@pixiv/three-vrm';
|
|
2
|
+
import { Object3D, Object3DEventMap } from 'three';
|
|
3
|
+
import { GLTFLoader } from 'three/examples/jsm/Addons.js';
|
|
4
|
+
export declare const vrmaLoader: GLTFLoader;
|
|
5
|
+
export declare function loadVrmCharacterModel(url: string): Promise<VRM & {
|
|
6
|
+
scene: Object3D<Object3DEventMap & {
|
|
7
|
+
dispose: {};
|
|
8
|
+
}>;
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { VRMLoaderPlugin, VRMUtils } from '@pixiv/three-vrm';
|
|
2
|
+
import { VRMAnimationLoaderPlugin } from '@pixiv/three-vrm-animation';
|
|
3
|
+
import { GLTFLoader } from 'three/examples/jsm/Addons.js';
|
|
4
|
+
export const vrmaLoader = new GLTFLoader();
|
|
5
|
+
vrmaLoader.register((parser) => new VRMLoaderPlugin(parser, { autoUpdateHumanBones: true }));
|
|
6
|
+
vrmaLoader.register((parser) => new VRMAnimationLoaderPlugin(parser));
|
|
7
|
+
export async function loadVrmCharacterModel(url) {
|
|
8
|
+
const vrm = (await vrmaLoader.loadAsync(url)).userData.vrm;
|
|
9
|
+
// fixes a bug where 2 VRMHumanoidRigs are loaded
|
|
10
|
+
vrm.scene.children
|
|
11
|
+
.filter((child) => child.name === 'VRMHumanoidRig')
|
|
12
|
+
.slice(0, -1)
|
|
13
|
+
.forEach((child) => child.parent?.remove(child));
|
|
14
|
+
VRMUtils.removeUnnecessaryVertices(vrm.scene);
|
|
15
|
+
VRMUtils.combineSkeletons(vrm.scene);
|
|
16
|
+
// Disable frustum culling
|
|
17
|
+
vrm.scene.traverse((obj) => void (obj.frustumCulled = false));
|
|
18
|
+
return vrm;
|
|
19
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Object3D, Vector3 } from 'three';
|
|
2
|
+
import { BvhPhysicsWorld } from './world.js';
|
|
3
|
+
export type BvhCharacterPhysicsOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* @default 60
|
|
6
|
+
*/
|
|
7
|
+
updatesPerSecond?: number;
|
|
8
|
+
/**
|
|
9
|
+
* @default 0.4
|
|
10
|
+
*/
|
|
11
|
+
capsuleRadius?: number;
|
|
12
|
+
/**
|
|
13
|
+
* @default 1.7
|
|
14
|
+
*/
|
|
15
|
+
capsuleHeight?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Gravity acceleration in m/s²
|
|
18
|
+
* @default -20
|
|
19
|
+
*/
|
|
20
|
+
gravity?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Linear damping coefficient (air resistance)
|
|
23
|
+
* @default 0.1
|
|
24
|
+
*/
|
|
25
|
+
linearDamping?: number;
|
|
26
|
+
/**
|
|
27
|
+
* @default 0.25;
|
|
28
|
+
*/
|
|
29
|
+
slopeGroundingThreshold?: number;
|
|
30
|
+
} | boolean;
|
|
31
|
+
/**
|
|
32
|
+
* assumes the target object origin is at its bottom
|
|
33
|
+
*/
|
|
34
|
+
export declare class BvhCharacterPhysics {
|
|
35
|
+
private readonly character;
|
|
36
|
+
private readonly world;
|
|
37
|
+
private destroyed;
|
|
38
|
+
private readonly stateVelocity;
|
|
39
|
+
readonly inputVelocity: Vector3;
|
|
40
|
+
isGrounded: boolean;
|
|
41
|
+
constructor(character: Object3D, world: BvhPhysicsWorld);
|
|
42
|
+
applyVelocity(velocity: Vector3): void;
|
|
43
|
+
/**
|
|
44
|
+
* @param delta in seconds
|
|
45
|
+
*/
|
|
46
|
+
update(fullDelta: number, options?: BvhCharacterPhysicsOptions): void;
|
|
47
|
+
destroy(): void;
|
|
48
|
+
shapecastCapsule(position: Vector3, options: Exclude<BvhCharacterPhysicsOptions, boolean>): void;
|
|
49
|
+
}
|
|
50
|
+
export * from './world.js';
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Box3, Line3, Matrix4, Vector3 } from 'three';
|
|
2
|
+
//for this is a kinematic character controller
|
|
3
|
+
//helper variables
|
|
4
|
+
const aabbox = new Box3();
|
|
5
|
+
const segment = new Line3();
|
|
6
|
+
const triPoint = new Vector3();
|
|
7
|
+
const capsulePoint = new Vector3();
|
|
8
|
+
const collisionFreePosition = new Vector3();
|
|
9
|
+
const position = new Vector3();
|
|
10
|
+
const collisionDelta = new Vector3();
|
|
11
|
+
const invertedParentMatrix = new Matrix4();
|
|
12
|
+
/**
|
|
13
|
+
* assumes the target object origin is at its bottom
|
|
14
|
+
*/
|
|
15
|
+
export class BvhCharacterPhysics {
|
|
16
|
+
character;
|
|
17
|
+
world;
|
|
18
|
+
destroyed = false;
|
|
19
|
+
stateVelocity = new Vector3();
|
|
20
|
+
inputVelocity = new Vector3();
|
|
21
|
+
isGrounded = false;
|
|
22
|
+
constructor(character, world) {
|
|
23
|
+
this.character = character;
|
|
24
|
+
this.world = world;
|
|
25
|
+
}
|
|
26
|
+
applyVelocity(velocity) {
|
|
27
|
+
this.stateVelocity.add(velocity);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* @param delta in seconds
|
|
31
|
+
*/
|
|
32
|
+
update(fullDelta, options = true) {
|
|
33
|
+
if (options === false) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (options === true) {
|
|
37
|
+
options = {};
|
|
38
|
+
}
|
|
39
|
+
if (this.destroyed) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
//at max catch up to 1 second of physics in one update call (running at less then 1fps is unplayable anyways)
|
|
43
|
+
fullDelta = Math.min(1, fullDelta);
|
|
44
|
+
const updatesPerSecond = options.updatesPerSecond ?? 60;
|
|
45
|
+
const physicsDelta = 1 / updatesPerSecond;
|
|
46
|
+
//strong simplified fixed physics update: we compute a frame for the fractional
|
|
47
|
+
while (fullDelta > 0) {
|
|
48
|
+
const partialDelta = Math.min(fullDelta, physicsDelta);
|
|
49
|
+
fullDelta -= physicsDelta;
|
|
50
|
+
//compute global position and inverted parent matrix so that we can compute the position in global space and re-assign it to the local chracter space
|
|
51
|
+
if (this.character.parent != null) {
|
|
52
|
+
this.character.parent.updateWorldMatrix(true, false);
|
|
53
|
+
position.copy(this.character.position).applyMatrix4(this.character.parent.matrixWorld);
|
|
54
|
+
invertedParentMatrix.copy(this.character.parent.matrixWorld).invert();
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
invertedParentMatrix.identity();
|
|
58
|
+
}
|
|
59
|
+
//compute new position based on the state velocity, the input velocity, and the delta
|
|
60
|
+
const yMovement = this.stateVelocity.y * partialDelta;
|
|
61
|
+
position.addScaledVector(this.stateVelocity, partialDelta);
|
|
62
|
+
position.addScaledVector(this.inputVelocity, partialDelta);
|
|
63
|
+
//compute collision and write the corrected position to the target
|
|
64
|
+
//TODO: rework - when are we on the ground and how to correct the shapecast
|
|
65
|
+
this.shapecastCapsule(collisionFreePosition.copy(position), options);
|
|
66
|
+
this.character.position.copy(collisionFreePosition).applyMatrix4(invertedParentMatrix);
|
|
67
|
+
//compute new velocity
|
|
68
|
+
// apply gravity
|
|
69
|
+
this.stateVelocity.y += (options.gravity ?? -20) * partialDelta;
|
|
70
|
+
// apply linear damping (air resistance)
|
|
71
|
+
const dampingFactor = 1.0 / (1.0 + partialDelta * (options.linearDamping ?? 0.1));
|
|
72
|
+
this.stateVelocity.multiplyScalar(dampingFactor);
|
|
73
|
+
// apply collision to velocity
|
|
74
|
+
collisionDelta.copy(collisionFreePosition).sub(position);
|
|
75
|
+
this.isGrounded = collisionDelta.y >= Math.abs(yMovement * (options.slopeGroundingThreshold ?? 0.6));
|
|
76
|
+
if (this.isGrounded) {
|
|
77
|
+
this.stateVelocity.set(0, (options.gravity ?? -20) * partialDelta, 0);
|
|
78
|
+
}
|
|
79
|
+
else if (collisionDelta.length() > 1e-5) {
|
|
80
|
+
collisionDelta.normalize();
|
|
81
|
+
this.stateVelocity.addScaledVector(collisionDelta, -collisionDelta.dot(this.stateVelocity));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
destroy() {
|
|
86
|
+
this.destroyed = true;
|
|
87
|
+
}
|
|
88
|
+
shapecastCapsule(position, options) {
|
|
89
|
+
const radius = options.capsuleRadius ?? 0.4;
|
|
90
|
+
const height = options.capsuleHeight ?? 1.7;
|
|
91
|
+
segment.start.copy(position);
|
|
92
|
+
segment.start.y += radius;
|
|
93
|
+
segment.end.copy(position);
|
|
94
|
+
segment.end.y += height - radius;
|
|
95
|
+
aabbox.makeEmpty();
|
|
96
|
+
aabbox.expandByPoint(segment.start);
|
|
97
|
+
aabbox.expandByPoint(segment.end);
|
|
98
|
+
aabbox.min.addScalar(-radius);
|
|
99
|
+
aabbox.max.addScalar(radius);
|
|
100
|
+
for (const bvh of this.world.getBodies()) {
|
|
101
|
+
bvh.shapecast({
|
|
102
|
+
intersectsBounds: (bounds) => bounds.intersectsBox(aabbox),
|
|
103
|
+
intersectsTriangle: (tri) => {
|
|
104
|
+
// Use your existing triangle vs segment closestPointToSegment
|
|
105
|
+
const distance = tri.closestPointToSegment(segment, triPoint, capsulePoint);
|
|
106
|
+
if (distance === 0) {
|
|
107
|
+
const scaledDirection = capsulePoint.sub(capsulePoint.distanceTo(segment.start) < capsulePoint.distanceTo(segment.end)
|
|
108
|
+
? segment.start
|
|
109
|
+
: segment.end);
|
|
110
|
+
scaledDirection.y += radius;
|
|
111
|
+
segment.start.add(scaledDirection);
|
|
112
|
+
segment.end.add(scaledDirection);
|
|
113
|
+
}
|
|
114
|
+
else if (distance < radius) {
|
|
115
|
+
const depthInsideCapsule = radius - distance;
|
|
116
|
+
const direction = capsulePoint.sub(triPoint).divideScalar(distance);
|
|
117
|
+
segment.start.addScaledVector(direction, depthInsideCapsule);
|
|
118
|
+
segment.end.addScaledVector(direction, depthInsideCapsule);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
position.copy(segment.start);
|
|
124
|
+
position.y -= radius;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export * from './world.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Object3D, Ray } from 'three';
|
|
2
|
+
import { MeshBVH } from 'three-mesh-bvh';
|
|
3
|
+
export declare class BvhPhysicsWorld {
|
|
4
|
+
private readonly map;
|
|
5
|
+
getBodies(): Iterable<MeshBVH>;
|
|
6
|
+
addFixedBody(object: Object3D): void;
|
|
7
|
+
removeFixedBody(object: Object3D): void;
|
|
8
|
+
raycast(ray: Ray, far: number): number | undefined;
|
|
9
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { computeBoundsTree, StaticGeometryGenerator } from 'three-mesh-bvh';
|
|
2
|
+
export class BvhPhysicsWorld {
|
|
3
|
+
map = new Map();
|
|
4
|
+
getBodies() {
|
|
5
|
+
return this.map.values();
|
|
6
|
+
}
|
|
7
|
+
addFixedBody(object) {
|
|
8
|
+
object.updateWorldMatrix(true, true);
|
|
9
|
+
const generator = new StaticGeometryGenerator(object);
|
|
10
|
+
this.map.set(object, computeBoundsTree.apply(generator.generate()));
|
|
11
|
+
}
|
|
12
|
+
removeFixedBody(object) {
|
|
13
|
+
this.map.delete(object);
|
|
14
|
+
}
|
|
15
|
+
raycast(ray, far) {
|
|
16
|
+
let result;
|
|
17
|
+
for (const body of this.getBodies()) {
|
|
18
|
+
for (const intersection of body.raycast(ray, undefined, 0, far)) {
|
|
19
|
+
if (result != null && intersection.distance >= result) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
result = intersection.distance;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { VRM } from '@pixiv/three-vrm';
|
|
2
|
+
import { AnimationAction, AnimationClip, AnimationMixer, Group, Object3D, Object3DEventMap, Quaternion } from 'three';
|
|
3
|
+
import { simpleCharacterAnimationNames, ModelAnimationOptions } from './animation/index.js';
|
|
4
|
+
import { SimpleCharacterCameraBehavior, SimpleCharacterCameraBehaviorOptions } from './camera.js';
|
|
5
|
+
import { Input, InputSystem } from './input/index.js';
|
|
6
|
+
import { CharacterModelOptions, loadCharacterModel } from './model/index.js';
|
|
7
|
+
import { BvhCharacterPhysicsOptions, BvhCharacterPhysics, BvhPhysicsWorld } from './physics/index.js';
|
|
8
|
+
export type SimpleCharacterMovementOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* @default true
|
|
11
|
+
*/
|
|
12
|
+
jump?: {
|
|
13
|
+
/**
|
|
14
|
+
* @default 0.2
|
|
15
|
+
*/
|
|
16
|
+
delay?: number;
|
|
17
|
+
/**
|
|
18
|
+
* @default 0.1
|
|
19
|
+
*/
|
|
20
|
+
bufferTime?: number;
|
|
21
|
+
/**
|
|
22
|
+
* @default 8
|
|
23
|
+
*/
|
|
24
|
+
speed?: number;
|
|
25
|
+
} | boolean;
|
|
26
|
+
/**
|
|
27
|
+
* @default true
|
|
28
|
+
*/
|
|
29
|
+
walk?: {
|
|
30
|
+
speed?: number;
|
|
31
|
+
} | boolean;
|
|
32
|
+
/**
|
|
33
|
+
* @default true
|
|
34
|
+
*/
|
|
35
|
+
run?: {
|
|
36
|
+
speed?: number;
|
|
37
|
+
} | boolean;
|
|
38
|
+
};
|
|
39
|
+
export type SimpleCharacterAnimationOptions = {
|
|
40
|
+
readonly walk?: ModelAnimationOptions;
|
|
41
|
+
readonly run?: ModelAnimationOptions;
|
|
42
|
+
readonly idle?: ModelAnimationOptions;
|
|
43
|
+
readonly jumpUp?: ModelAnimationOptions;
|
|
44
|
+
readonly jumpLoop?: ModelAnimationOptions;
|
|
45
|
+
readonly jumpDown?: ModelAnimationOptions;
|
|
46
|
+
readonly jumpForward?: ModelAnimationOptions;
|
|
47
|
+
/**
|
|
48
|
+
* @default "movement"
|
|
49
|
+
*/
|
|
50
|
+
yawRotationBasdOn?: 'camera' | 'movement';
|
|
51
|
+
/**
|
|
52
|
+
* @default 10
|
|
53
|
+
*/
|
|
54
|
+
maxYawRotationSpeed?: number;
|
|
55
|
+
/**
|
|
56
|
+
* @default 0.3
|
|
57
|
+
*/
|
|
58
|
+
crossFadeDuration?: number;
|
|
59
|
+
};
|
|
60
|
+
export type SimpleCharacterOptions = {
|
|
61
|
+
/**
|
|
62
|
+
* @default [LocomotionKeyboardInput,PointerCaptureInput]
|
|
63
|
+
*/
|
|
64
|
+
readonly input?: ReadonlyArray<Input | {
|
|
65
|
+
new (domElement: HTMLElement): Input;
|
|
66
|
+
}> | InputSystem;
|
|
67
|
+
movement?: SimpleCharacterMovementOptions;
|
|
68
|
+
readonly model?: CharacterModelOptions;
|
|
69
|
+
physics?: BvhCharacterPhysicsOptions;
|
|
70
|
+
cameraBehavior?: SimpleCharacterCameraBehaviorOptions;
|
|
71
|
+
readonly animation?: SimpleCharacterAnimationOptions;
|
|
72
|
+
};
|
|
73
|
+
export declare function preloadSimpleCharacterAssets(options: Pick<SimpleCharacterOptions, 'animation' | 'model'>): Promise<{
|
|
74
|
+
model?: undefined;
|
|
75
|
+
animations?: undefined;
|
|
76
|
+
} | {
|
|
77
|
+
model: ((VRM & {
|
|
78
|
+
scene: Object3D<Object3DEventMap & {
|
|
79
|
+
dispose: {};
|
|
80
|
+
}>;
|
|
81
|
+
}) | (import("three/examples/jsm/Addons.js").GLTF & {
|
|
82
|
+
scene: Object3D<Object3DEventMap & {
|
|
83
|
+
dispose: {};
|
|
84
|
+
}>;
|
|
85
|
+
})) & {
|
|
86
|
+
boneRotationOffset?: Quaternion;
|
|
87
|
+
};
|
|
88
|
+
animations: Record<"walk" | "run" | "jumpForward" | "idle" | "jumpUp" | "jumpLoop" | "jumpDown", AnimationClip>;
|
|
89
|
+
}>;
|
|
90
|
+
export declare class SimpleCharacter extends Group<Object3DEventMap & {
|
|
91
|
+
loaded: {};
|
|
92
|
+
}> {
|
|
93
|
+
readonly options: SimpleCharacterOptions;
|
|
94
|
+
readonly cameraBehavior: SimpleCharacterCameraBehavior;
|
|
95
|
+
readonly physics: BvhCharacterPhysics;
|
|
96
|
+
readonly mixer: AnimationMixer;
|
|
97
|
+
inputSystem: InputSystem;
|
|
98
|
+
actions?: Record<(typeof simpleCharacterAnimationNames)[number], AnimationAction> | undefined;
|
|
99
|
+
model?: Awaited<Exclude<ReturnType<typeof loadCharacterModel>, undefined>>;
|
|
100
|
+
private updateTimeline?;
|
|
101
|
+
constructor(camera: Object3D, world: BvhPhysicsWorld, domElement: HTMLElement, options?: SimpleCharacterOptions);
|
|
102
|
+
private init;
|
|
103
|
+
update(delta: number): void;
|
|
104
|
+
dispose(): void;
|
|
105
|
+
}
|