@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.
Files changed (60) hide show
  1. package/LICENSE +34 -0
  2. package/README.md +41 -0
  3. package/dist/animation/gltf.d.ts +3 -0
  4. package/dist/animation/gltf.js +8 -0
  5. package/dist/animation/index.d.ts +30 -0
  6. package/dist/animation/index.js +150 -0
  7. package/dist/animation/mixamo-bone-map.json +54 -0
  8. package/dist/animation/mixamo.d.ts +3 -0
  9. package/dist/animation/mixamo.js +9 -0
  10. package/dist/animation/utils.d.ts +3 -0
  11. package/dist/animation/utils.js +28 -0
  12. package/dist/animation/vrma.d.ts +3 -0
  13. package/dist/animation/vrma.js +8 -0
  14. package/dist/assets/idle.d.ts +1 -0
  15. package/dist/assets/idle.js +1 -0
  16. package/dist/assets/jump-down.d.ts +1 -0
  17. package/dist/assets/jump-down.js +1 -0
  18. package/dist/assets/jump-forward.d.ts +1 -0
  19. package/dist/assets/jump-forward.js +1 -0
  20. package/dist/assets/jump-loop.d.ts +1 -0
  21. package/dist/assets/jump-loop.js +1 -0
  22. package/dist/assets/jump-up.d.ts +1 -0
  23. package/dist/assets/jump-up.js +1 -0
  24. package/dist/assets/mannequin.d.ts +1 -0
  25. package/dist/assets/mannequin.js +1 -0
  26. package/dist/assets/prototype-texture.d.ts +1 -0
  27. package/dist/assets/prototype-texture.js +1 -0
  28. package/dist/assets/run.d.ts +1 -0
  29. package/dist/assets/run.js +1 -0
  30. package/dist/assets/walk.d.ts +1 -0
  31. package/dist/assets/walk.js +1 -0
  32. package/dist/camera.d.ts +79 -0
  33. package/dist/camera.js +139 -0
  34. package/dist/index.d.ts +7 -0
  35. package/dist/index.js +7 -0
  36. package/dist/input/index.d.ts +30 -0
  37. package/dist/input/index.js +64 -0
  38. package/dist/input/keyboard.d.ts +8 -0
  39. package/dist/input/keyboard.js +77 -0
  40. package/dist/input/pointer-capture.d.ts +14 -0
  41. package/dist/input/pointer-capture.js +59 -0
  42. package/dist/input/pointer-lock.d.ts +13 -0
  43. package/dist/input/pointer-lock.js +51 -0
  44. package/dist/material.d.ts +6 -0
  45. package/dist/material.js +18 -0
  46. package/dist/model/gltf.d.ts +8 -0
  47. package/dist/model/gltf.js +7 -0
  48. package/dist/model/index.d.ts +32 -0
  49. package/dist/model/index.js +57 -0
  50. package/dist/model/vrm.d.ts +9 -0
  51. package/dist/model/vrm.js +19 -0
  52. package/dist/physics/index.d.ts +50 -0
  53. package/dist/physics/index.js +127 -0
  54. package/dist/physics/world.d.ts +9 -0
  55. package/dist/physics/world.js +27 -0
  56. package/dist/simple-character.d.ts +105 -0
  57. package/dist/simple-character.js +329 -0
  58. package/dist/utils.d.ts +2 -0
  59. package/dist/utils.js +34 -0
  60. package/package.json +47 -0
@@ -0,0 +1,329 @@
1
+ import { VRM, VRMUtils } from '@pixiv/three-vrm';
2
+ import { action, animationFinished, build, timePassed, forever, parallel, graph, } from '@pmndrs/timeline';
3
+ import { AnimationMixer, Euler, Group, LoopOnce, Quaternion, Vector3, } from 'three';
4
+ import { simpleCharacterAnimationNames, getSimpleCharacterModelAnimationOptions as getSimpleCharacterModelAnimationOptions, loadCharacterModelAnimation as loadCharacterModelAnimation, } from './animation/index.js';
5
+ import { SimpleCharacterCameraBehavior } from './camera.js';
6
+ import { InputSystem, LocomotionKeyboardInput, MoveBackwardField, MoveForwardField, MoveLeftField, MoveRightField, PointerCaptureInput, RunField, LastTimeJumpPressedField, } from './input/index.js';
7
+ import { clearCharacterModelCache, loadCharacterModel } from './model/index.js';
8
+ import { BvhCharacterPhysics } from './physics/index.js';
9
+ const DefaultCrossFadeDuration = 0.1;
10
+ const DefaultJumDelay = 0.2;
11
+ //constants
12
+ const NegZAxis = new Vector3(0, 0, -1);
13
+ const _2MathPI = 2 * Math.PI;
14
+ //helper objects
15
+ const cameraEuler = new Euler();
16
+ const cameraRotation = new Quaternion();
17
+ const vector = new Vector3();
18
+ const characterTargetEuler = new Euler();
19
+ const goalTargetEuler = new Euler();
20
+ const inputDirection = new Vector3();
21
+ const quaternion = new Quaternion();
22
+ export async function preloadSimpleCharacterAssets(options) {
23
+ // load model
24
+ const model = await loadCharacterModel(options.model);
25
+ if (model == null) {
26
+ return {};
27
+ }
28
+ model.scene.addEventListener('dispose', () => clearCharacterModelCache(options.model));
29
+ // load animations
30
+ return {
31
+ model,
32
+ animations: (await Promise.all(simpleCharacterAnimationNames.map(async (name) => loadCharacterModelAnimation(model, options.animation?.[name] ?? (await getSimpleCharacterModelAnimationOptions(name)))))).reduce((prev, animation, i) => {
33
+ prev[simpleCharacterAnimationNames[i]] = animation;
34
+ return prev;
35
+ }, {}),
36
+ };
37
+ }
38
+ async function* SimpleCharacterTimeline(camera, character) {
39
+ let lastJump = 0;
40
+ function shouldJump() {
41
+ let jumpOptions = character.options.movement?.jump;
42
+ if (jumpOptions === false) {
43
+ return false;
44
+ }
45
+ if (jumpOptions === true) {
46
+ jumpOptions = {};
47
+ }
48
+ if (!character.physics.isGrounded) {
49
+ return false;
50
+ }
51
+ const lastTimePressed = character.inputSystem.get(LastTimeJumpPressedField);
52
+ if (lastTimePressed == null) {
53
+ return false;
54
+ }
55
+ if (lastJump > lastTimePressed) {
56
+ return false;
57
+ }
58
+ return performance.now() / 1000 - lastTimePressed < (jumpOptions?.bufferTime ?? 0.1);
59
+ }
60
+ function applyJumpForce() {
61
+ character.physics.applyVelocity(vector.set(0, (typeof character.options.movement?.jump === 'object' ? character.options.movement?.jump.speed : undefined) ??
62
+ 8, 0));
63
+ }
64
+ const model = character.model;
65
+ const actions = character.actions;
66
+ //run character
67
+ yield* parallel('all',
68
+ // character movement
69
+ action({
70
+ update() {
71
+ cameraEuler.setFromQuaternion(camera.getWorldQuaternion(cameraRotation), 'YXZ');
72
+ cameraEuler.x = 0;
73
+ cameraEuler.z = 0;
74
+ let inputSpeed = 0;
75
+ let runOptions = character.options.movement?.run ?? true;
76
+ if (character.inputSystem.get(RunField) && runOptions !== false) {
77
+ runOptions = runOptions === true ? {} : runOptions;
78
+ inputSpeed = runOptions.speed ?? 6;
79
+ }
80
+ let walkOptions = character.options.movement?.walk ?? true;
81
+ if (inputSpeed === 0 && walkOptions !== false) {
82
+ walkOptions = walkOptions === true ? {} : walkOptions;
83
+ inputSpeed = walkOptions.speed ?? 3;
84
+ }
85
+ character.physics.inputVelocity
86
+ .set(-character.inputSystem.get(MoveLeftField) + character.inputSystem.get(MoveRightField), 0, -character.inputSystem.get(MoveForwardField) + character.inputSystem.get(MoveBackwardField))
87
+ .normalize()
88
+ .applyEuler(cameraEuler)
89
+ .multiplyScalar(inputSpeed);
90
+ //run forever
91
+ return true;
92
+ },
93
+ }),
94
+ // rotation animations
95
+ model != null &&
96
+ action({
97
+ update(_, clock) {
98
+ // Character yaw rotation logic
99
+ const basedOn = character.options.animation?.yawRotationBasdOn ?? 'movement';
100
+ // compute goalTargetEuler
101
+ if (basedOn === 'camera') {
102
+ goalTargetEuler.setFromQuaternion(camera.getWorldQuaternion(quaternion), 'YXZ');
103
+ }
104
+ else {
105
+ //don't rotate if not moving
106
+ if (character.physics.inputVelocity.lengthSq() === 0) {
107
+ // run forever
108
+ return true;
109
+ }
110
+ inputDirection.copy(character.physics.inputVelocity).normalize();
111
+ quaternion.setFromUnitVectors(NegZAxis, inputDirection);
112
+ goalTargetEuler.setFromQuaternion(quaternion, 'YXZ');
113
+ }
114
+ // compute currentTargetEuler
115
+ model.scene.getWorldQuaternion(quaternion);
116
+ characterTargetEuler.setFromQuaternion(quaternion, 'YXZ');
117
+ // apply delta yaw rotation
118
+ let deltaYaw = (goalTargetEuler.y - characterTargetEuler.y + _2MathPI) % _2MathPI;
119
+ if (deltaYaw > Math.PI) {
120
+ deltaYaw = deltaYaw - _2MathPI;
121
+ }
122
+ const absDeltaYaw = Math.abs(deltaYaw);
123
+ if (absDeltaYaw < 0.001) {
124
+ // run forever
125
+ return true;
126
+ }
127
+ const yawRotationDirection = deltaYaw / absDeltaYaw;
128
+ const maxYawRotationSpeed = (typeof character.options.animation === 'object'
129
+ ? character.options.animation.maxYawRotationSpeed
130
+ : undefined) ?? 10;
131
+ model.scene.rotation.y += Math.min(maxYawRotationSpeed * clock.delta, absDeltaYaw) * yawRotationDirection;
132
+ // run forever
133
+ return true;
134
+ },
135
+ }),
136
+ // jump and walk animations
137
+ actions == null
138
+ ? action({
139
+ update: () => void (shouldJump() && applyJumpForce()),
140
+ })
141
+ : graph('moving', {
142
+ jumpStart: {
143
+ timeline: async function* () {
144
+ yield* action({
145
+ init() {
146
+ actions.jumpUp.reset();
147
+ actions.jumpUp.play();
148
+ actions.jumpUp.paused = true;
149
+ actions.jumpUp.fadeIn(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration);
150
+ actions.jumpForward.reset();
151
+ actions.jumpForward.play();
152
+ actions.jumpForward.paused = true;
153
+ actions.jumpForward.fadeIn(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration);
154
+ },
155
+ update: () => void character.physics.inputVelocity.multiplyScalar(DefaultCrossFadeDuration),
156
+ until: timePassed((typeof character.options.movement?.jump === 'object'
157
+ ? character.options.movement?.jump.delay
158
+ : undefined) ?? DefaultJumDelay, 'seconds'),
159
+ });
160
+ if (character.inputSystem.get(RunField)) {
161
+ actions.jumpUp.fadeOut(0.1);
162
+ return 'jumpForward';
163
+ }
164
+ else {
165
+ actions.jumpForward.fadeOut(0.1);
166
+ return 'jumpUp';
167
+ }
168
+ },
169
+ transitionTo: {
170
+ jumpDown: { when: () => !character.physics.isGrounded },
171
+ },
172
+ },
173
+ jumpForward: {
174
+ timeline: async function* () {
175
+ yield* action({
176
+ init: () => {
177
+ actions.jumpForward.paused = false;
178
+ applyJumpForce();
179
+ },
180
+ cleanup: () => void actions.jumpForward.fadeOut(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration),
181
+ until: animationFinished(actions.jumpForward),
182
+ });
183
+ if (character.physics.isGrounded) {
184
+ return 'moving';
185
+ }
186
+ return 'jumpLoop';
187
+ },
188
+ },
189
+ jumpUp: {
190
+ timeline: () => action({
191
+ init: () => {
192
+ actions.jumpUp.paused = false;
193
+ applyJumpForce();
194
+ },
195
+ cleanup: () => void actions.jumpUp.fadeOut(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration),
196
+ until: animationFinished(actions.jumpUp),
197
+ }),
198
+ transitionTo: {
199
+ jumpDown: {
200
+ when: (_, clock) => clock.actionTime > 0.1 && character.physics.isGrounded,
201
+ },
202
+ finally: 'jumpLoop',
203
+ },
204
+ },
205
+ jumpLoop: {
206
+ timeline: () => action({
207
+ init: () => {
208
+ actions.jumpLoop.reset();
209
+ actions.jumpLoop.play();
210
+ actions.jumpLoop.fadeIn(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration);
211
+ },
212
+ cleanup: () => actions.jumpLoop.fadeOut(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration),
213
+ until: forever(),
214
+ }),
215
+ transitionTo: {
216
+ jumpDown: { when: () => character.physics.isGrounded },
217
+ },
218
+ },
219
+ jumpDown: {
220
+ timeline: () => action({
221
+ init: () => {
222
+ actions.jumpUp.fadeOut(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration);
223
+ actions.jumpForward.fadeOut(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration);
224
+ actions.jumpDown.reset();
225
+ actions.jumpDown.play();
226
+ actions.jumpDown.fadeIn(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration);
227
+ },
228
+ cleanup: () => actions.jumpDown.fadeOut(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration),
229
+ until: timePassed(50, 'milliseconds'),
230
+ }),
231
+ transitionTo: { finally: 'moving' },
232
+ },
233
+ moving: {
234
+ timeline: () => {
235
+ let currentAnimation;
236
+ return action({
237
+ update() {
238
+ let nextAnimation;
239
+ if (character.physics.inputVelocity.lengthSq() === 0) {
240
+ nextAnimation = actions.idle;
241
+ }
242
+ else if (character.inputSystem.get(RunField) && character.options.movement?.run != false) {
243
+ nextAnimation = actions.run;
244
+ }
245
+ else if (character.options.movement?.walk != false) {
246
+ nextAnimation = actions.walk;
247
+ }
248
+ else {
249
+ nextAnimation = actions.idle;
250
+ }
251
+ if (nextAnimation === currentAnimation) {
252
+ return;
253
+ }
254
+ currentAnimation?.fadeOut(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration);
255
+ nextAnimation.reset();
256
+ nextAnimation.play();
257
+ nextAnimation.fadeIn(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration);
258
+ currentAnimation = nextAnimation;
259
+ },
260
+ cleanup: () => currentAnimation?.fadeOut(character.options.animation?.crossFadeDuration ?? DefaultCrossFadeDuration),
261
+ });
262
+ },
263
+ transitionTo: {
264
+ jumpStart: { when: () => shouldJump() },
265
+ jumpLoop: { when: () => !character.physics.isGrounded },
266
+ },
267
+ },
268
+ }));
269
+ }
270
+ export class SimpleCharacter extends Group {
271
+ options;
272
+ cameraBehavior;
273
+ physics;
274
+ mixer = new AnimationMixer(this);
275
+ //can be changed from the outside
276
+ inputSystem;
277
+ //loaded asychronously
278
+ actions;
279
+ model;
280
+ updateTimeline;
281
+ constructor(camera, world, domElement, options = {}) {
282
+ super();
283
+ this.options = options;
284
+ // input system
285
+ this.inputSystem =
286
+ options.input instanceof InputSystem
287
+ ? options.input
288
+ : new InputSystem(domElement, options.input ?? [LocomotionKeyboardInput, PointerCaptureInput]);
289
+ options.physics ??= {};
290
+ // camera behavior
291
+ this.cameraBehavior = new SimpleCharacterCameraBehavior(camera, this, this.inputSystem, world.raycast.bind(world));
292
+ // physics
293
+ this.physics = new BvhCharacterPhysics(this, world);
294
+ this.init(camera, options).catch(console.error);
295
+ }
296
+ async init(camera, options) {
297
+ const { model, animations } = await preloadSimpleCharacterAssets(options);
298
+ this.model = model;
299
+ if (model != null && animations != null) {
300
+ this.add(model.scene);
301
+ this.actions = {};
302
+ for (const name of simpleCharacterAnimationNames) {
303
+ this.actions[name] = this.mixer.clipAction(animations[name]);
304
+ }
305
+ this.actions.jumpDown.loop = LoopOnce;
306
+ this.actions.jumpDown.clampWhenFinished = true;
307
+ this.actions.jumpUp.loop = LoopOnce;
308
+ this.actions.jumpUp.clampWhenFinished = true;
309
+ this.actions.jumpForward.loop = LoopOnce;
310
+ this.actions.jumpForward.clampWhenFinished = true;
311
+ }
312
+ this.updateTimeline = build(SimpleCharacterTimeline(camera, this));
313
+ this.dispatchEvent({ type: 'loaded' });
314
+ }
315
+ update(delta) {
316
+ this.updateTimeline?.(undefined, delta);
317
+ this.mixer?.update(delta);
318
+ if (this.model instanceof VRM) {
319
+ this.model.update(delta);
320
+ }
321
+ this.physics.update(delta, this.options.physics);
322
+ this.cameraBehavior.update(delta, this.options.cameraBehavior);
323
+ }
324
+ dispose() {
325
+ this.parent?.remove(this);
326
+ this.model?.scene.dispatchEvent({ type: 'dispose' });
327
+ VRMUtils.deepDispose(this);
328
+ }
329
+ }
@@ -0,0 +1,2 @@
1
+ export declare function cached<D extends ReadonlyArray<unknown>, T>(fn: (...deps: D) => Promise<T>, dependencies: D): Promise<T>;
2
+ export declare function clearCache(fn: Function, dependencies: Array<unknown>): void;
package/dist/utils.js ADDED
@@ -0,0 +1,34 @@
1
+ function shallowEqual(a, b) {
2
+ if (a.length !== b.length)
3
+ return false;
4
+ for (let i = 0; i < a.length; i++) {
5
+ if (a[i] !== b[i])
6
+ return false;
7
+ }
8
+ return true;
9
+ }
10
+ const cacheMap = new Map();
11
+ export function cached(fn, dependencies) {
12
+ let cache = cacheMap.get(fn);
13
+ if (cache == null) {
14
+ cacheMap.set(fn, (cache = []));
15
+ }
16
+ const entry = cache.find(({ deps }) => shallowEqual(deps, dependencies));
17
+ if (entry != null) {
18
+ return entry.result;
19
+ }
20
+ const result = fn(...dependencies);
21
+ cache.push({ deps: dependencies, result });
22
+ return result;
23
+ }
24
+ export function clearCache(fn, dependencies) {
25
+ const cache = cacheMap.get(fn);
26
+ if (cache == null) {
27
+ return;
28
+ }
29
+ const index = cache.findIndex(({ deps }) => shallowEqual(deps, dependencies));
30
+ if (index === -1) {
31
+ return;
32
+ }
33
+ cache.splice(index, 1);
34
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@pmndrs/viverse",
3
+ "description": "Toolkit for building and publishing Threejs Apps to Viverse.",
4
+ "author": "Bela Bohlender",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "homepage": "https://github.com/pmndrs/viverse",
7
+ "keywords": [
8
+ "viverse",
9
+ "character controller",
10
+ "three.js",
11
+ "typescript",
12
+ "web game"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git@github.com:pmndrs/viverse.git"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "peerDependencies": {
22
+ "three": "*"
23
+ },
24
+ "version": "0.1.2",
25
+ "type": "module",
26
+ "dependencies": {
27
+ "@pixiv/three-vrm": "^3.4.2",
28
+ "@pixiv/three-vrm-animation": "^3.4.2",
29
+ "@pmndrs/timeline": "^0.1.10",
30
+ "@viverse/sdk": "1.2.10-alpha.0",
31
+ "three": "*",
32
+ "three-mesh-bvh": "^0.9.1"
33
+ },
34
+ "devDependencies": {
35
+ "ts-node": "^10.9.2",
36
+ "@types/node": "^24.0.14"
37
+ },
38
+ "main": "dist/index.js",
39
+ "scripts": {
40
+ "generate": "node --loader ts-node/esm scripts/build-assets.ts",
41
+ "build": "tsc -p tsconfig.build.json",
42
+ "check:prettier": "prettier --check src",
43
+ "check:eslint": "eslint \"src/**/*.{ts,tsx}\"",
44
+ "fix:prettier": "prettier --write src",
45
+ "fix:eslint": "eslint \"src/**/*.{ts,tsx}\" --fix"
46
+ }
47
+ }