@react-three/viverse 0.2.0 → 0.2.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/README.md CHANGED
@@ -61,9 +61,10 @@ Get started with **[building a simple game](https://pmndrs.github.io/viverse/tut
61
61
  - [Accessing avatar and profile](https://pmndrs.github.io/viverse/tutorials/access-avatar-and-profile)
62
62
  - [Equipping the character with items](https://pmndrs.github.io/viverse/tutorials/equipping-items)
63
63
  - [Using custom animations and models](https://pmndrs.github.io/viverse/tutorials/custom-models-and-animations)
64
+ - [Actions](https://pmndrs.github.io/viverse/tutorials/actions)
65
+ - [Custom Character Controller](https://pmndrs.github.io/viverse/tutorials/custom-character-controller)
64
66
  - [How to remove the viverse integrations](https://pmndrs.github.io/viverse/tutorials/remove-viverse-integrations)
65
67
  - [Publish to VIVERSE](https://pmndrs.github.io/viverse/tutorials/publish-to-viverse)
66
- - Building your own character controller - _Coming Soon_
67
68
 
68
69
  ## Not into react?
69
70
 
@@ -1,9 +1,27 @@
1
1
  import { CharacterAnimationOptions, StartAnimationOptions } from '@pmndrs/viverse';
2
2
  import { RootState } from '@react-three/fiber';
3
3
  import { ActionParams } from '@react-three/timeline';
4
- import { AnimationActionLoopStyles } from 'three';
5
- export declare function CharacterAnimationAction({ until, init, dependencies, update, fadeDuration, paused, loop, ...animationOptions }: {
4
+ import { ReactNode } from 'react';
5
+ import { AnimationAction, AnimationActionLoopStyles, AnimationClip } from 'three';
6
+ export type AdditiveCharacterAnimationActionProps = Omit<CharacterAnimationActionProps, 'additiveToClip'> & {
7
+ referenceClip: CharacterAnimationOptions;
8
+ };
9
+ export declare const AdditiveCharacterAnimationAction: import("react").ForwardRefExoticComponent<Omit<CharacterAnimationActionProps, "additiveToClip"> & {
10
+ referenceClip: CharacterAnimationOptions;
11
+ } & import("react").RefAttributes<AnimationAction>>;
12
+ export type CharacterAnimationActionProps = {
6
13
  dependencies?: Array<unknown>;
7
14
  until?: () => Promise<unknown>;
8
15
  loop?: AnimationActionLoopStyles;
9
- } & Omit<ActionParams<RootState>, 'until'> & CharacterAnimationOptions & StartAnimationOptions): import("react/jsx-runtime").JSX.Element;
16
+ additiveReferenceClip?: AnimationClip;
17
+ } & Omit<ActionParams<RootState>, 'until'> & CharacterAnimationOptions & StartAnimationOptions;
18
+ export declare function CharacterAnimationLayer({ name, children }: {
19
+ name: string;
20
+ children?: ReactNode;
21
+ }): import("react/jsx-runtime").JSX.Element;
22
+ export declare const CharacterAnimationAction: import("react").ForwardRefExoticComponent<{
23
+ dependencies?: Array<unknown>;
24
+ until?: () => Promise<unknown>;
25
+ loop?: AnimationActionLoopStyles;
26
+ additiveReferenceClip?: AnimationClip;
27
+ } & Omit<ActionParams<RootState>, "until"> & CharacterAnimationOptions & StartAnimationOptions & import("react").RefAttributes<AnimationAction>>;
package/dist/animation.js CHANGED
@@ -1,18 +1,44 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { startAnimation } from '@pmndrs/viverse';
3
3
  import { Action, animationFinished } from '@react-three/timeline';
4
- import { useMemo } from 'react';
4
+ import { createContext, forwardRef, useContext, useImperativeHandle, useMemo } from 'react';
5
5
  import { LoopRepeat } from 'three';
6
+ import { makeClipAdditive } from 'three/src/animation/AnimationUtils.js';
6
7
  import { useCharacterModel } from './model.js';
7
8
  import { useCharacterAnimationLoader } from './utils.js';
8
- export function CharacterAnimationAction({ until, init, dependencies, update, fadeDuration = 0.1, paused, loop, ...animationOptions }) {
9
+ export const AdditiveCharacterAnimationAction = forwardRef(({ referenceClip: referenceClipOptions, ...props }, ref) => {
9
10
  const model = useCharacterModel();
10
- const clip = useCharacterAnimationLoader(model, animationOptions);
11
+ const referenceClip = useCharacterAnimationLoader(model, { ...props, ...referenceClipOptions });
12
+ return _jsx(CharacterAnimationAction, { ref: ref, additiveReferenceClip: referenceClip, ...props });
13
+ });
14
+ const CharacterAnimationLayerContext = createContext(undefined);
15
+ export function CharacterAnimationLayer({ name, children }) {
16
+ return _jsx(CharacterAnimationLayerContext.Provider, { value: name, children: children });
17
+ }
18
+ export const CharacterAnimationAction = forwardRef(({ additiveReferenceClip, until, init, dependencies, update, fadeDuration, paused, loop, sync, crossFade, layer, ...animationOptions }, ref) => {
19
+ const layerFromContext = useContext(CharacterAnimationLayerContext);
20
+ layer ??= layerFromContext;
21
+ const model = useCharacterModel();
22
+ const srcClip = useCharacterAnimationLoader(model, animationOptions);
23
+ const clip = useMemo(() => additiveReferenceClip == null ? srcClip : makeClipAdditive(srcClip.clone(), undefined, additiveReferenceClip), [srcClip, additiveReferenceClip]);
11
24
  const animation = useMemo(() => model.mixer.clipAction(clip), [clip, model]);
12
25
  animation.clampWhenFinished = true;
13
26
  animation.loop = loop ?? LoopRepeat;
27
+ useImperativeHandle(ref, () => animation, [animation]);
14
28
  return (_jsx(Action, { init: () => {
15
- startAnimation(animation, model.currentAnimations, { fadeDuration, paused });
16
- return init?.();
17
- }, until: until ?? (() => animationFinished(animation)), update: update, dependencies: dependencies }));
18
- }
29
+ const cleanupAnimation = startAnimation(animation, model.currentAnimations, {
30
+ layer,
31
+ fadeDuration,
32
+ paused,
33
+ sync,
34
+ crossFade,
35
+ });
36
+ const cleanupInit = init?.();
37
+ return () => {
38
+ cleanupInit?.();
39
+ cleanupAnimation?.();
40
+ };
41
+ }, until: until ?? (() => animationFinished(animation)), update: update, dependencies: dependencies != null
42
+ ? [...dependencies, animationOptions.mask, fadeDuration, paused, sync, crossFade, animation, model, layer]
43
+ : undefined }));
44
+ });
package/dist/bone.d.ts CHANGED
@@ -4,3 +4,7 @@ export declare function CharacterModelBone({ bone, children }: {
4
4
  bone: VRMHumanBoneName;
5
5
  children?: ReactNode;
6
6
  }): import("react/jsx-runtime").JSX.Element | null;
7
+ /**
8
+ * @deprecated use CharacterModelBone instead
9
+ */
10
+ export declare const VrmCharacterModelBone: typeof CharacterModelBone;
package/dist/bone.js CHANGED
@@ -12,3 +12,7 @@ export function CharacterModelBone({ bone, children }) {
12
12
  }
13
13
  return (_jsx(Fragment, { children: createPortal(_jsx("group", { quaternion: model.boneRotationOffset, children: children }), boneObject) }, boneObject.id));
14
14
  }
15
+ /**
16
+ * @deprecated use CharacterModelBone instead
17
+ */
18
+ export const VrmCharacterModelBone = CharacterModelBone;
package/dist/gamepad.d.ts CHANGED
@@ -1,2 +1 @@
1
- import { Input } from '@pmndrs/viverse';
2
- export declare function useXRControllerInput(): Input<{}>;
1
+ export declare function useXRControllerLocomotionActionBindings(): void;
package/dist/gamepad.js CHANGED
@@ -1,38 +1,27 @@
1
- import { MoveForwardField, LastTimeJumpPressedField, MoveLeftField, MoveRightField, MoveBackwardField, RunField, } from '@pmndrs/viverse';
1
+ import { JumpAction, MoveBackwardAction, MoveForwardAction, MoveLeftAction, MoveRightAction, } from '@pmndrs/viverse';
2
+ import { useFrame } from '@react-three/fiber';
2
3
  import { useXRControllerButtonEvent, useXRInputSourceState } from '@react-three/xr';
3
- import { useMemo, useRef } from 'react';
4
- export function useXRControllerInput() {
4
+ import { useEffect, useRef } from 'react';
5
+ export function useXRControllerLocomotionActionBindings() {
5
6
  const leftController = useXRInputSourceState('controller', 'left');
6
- const lastAPressed = useRef(null);
7
7
  const rightController = useXRInputSourceState('controller', 'right');
8
- useXRControllerButtonEvent(rightController, 'a-button', (state) => state === 'pressed' && (lastAPressed.current = performance.now() / 1000));
9
- return useMemo(() => ({
10
- get(field) {
11
- switch (field) {
12
- case MoveForwardField:
13
- case MoveBackwardField: {
14
- const thumbstickYAxis = leftController?.gamepad?.['xr-standard-thumbstick']?.yAxis;
15
- if (thumbstickYAxis == null) {
16
- return undefined;
17
- }
18
- return field === MoveBackwardField
19
- ? Math.max(0, thumbstickYAxis)
20
- : Math.max(0, -thumbstickYAxis);
21
- }
22
- case MoveLeftField:
23
- case MoveRightField: {
24
- const thumbstickXAxis = leftController?.gamepad?.['xr-standard-thumbstick']?.xAxis;
25
- if (thumbstickXAxis == null) {
26
- return undefined;
27
- }
28
- return field === MoveLeftField ? Math.max(0, -thumbstickXAxis) : Math.max(0, thumbstickXAxis);
29
- }
30
- case LastTimeJumpPressedField:
31
- return lastAPressed.current;
32
- case RunField:
33
- return (leftController?.gamepad?.['xr-standard-trigger']?.state === 'pressed');
34
- }
35
- return undefined;
36
- },
37
- }), [leftController]);
8
+ useXRControllerButtonEvent(rightController, 'a-button', (state) => state === 'pressed' && JumpAction.emit(undefined));
9
+ const forwardWriterRef = useRef(undefined);
10
+ const backwardWriterRef = useRef(undefined);
11
+ const leftWriterRef = useRef(undefined);
12
+ const rightWriterRef = useRef(undefined);
13
+ useEffect(() => {
14
+ const abortController = new AbortController();
15
+ forwardWriterRef.current = MoveForwardAction.createWriter(abortController.signal);
16
+ backwardWriterRef.current = MoveBackwardAction.createWriter(abortController.signal);
17
+ leftWriterRef.current = MoveLeftAction.createWriter(abortController.signal);
18
+ rightWriterRef.current = MoveRightAction.createWriter(abortController.signal);
19
+ return () => abortController.abort();
20
+ }, [leftController]);
21
+ useFrame(() => {
22
+ forwardWriterRef.current?.write(-Math.min(0, leftController?.gamepad?.['xr-standard-thumbstick']?.yAxis ?? 0));
23
+ backwardWriterRef.current?.write(Math.max(0, leftController?.gamepad?.['xr-standard-thumbstick']?.yAxis ?? 0));
24
+ leftWriterRef.current?.write(-Math.min(0, leftController?.gamepad?.['xr-standard-thumbstick']?.xAxis ?? 0));
25
+ rightWriterRef.current?.write(Math.max(0, leftController?.gamepad?.['xr-standard-thumbstick']?.xAxis ?? 0));
26
+ });
38
27
  }
package/dist/index.d.ts CHANGED
@@ -69,6 +69,8 @@ export declare function useViversePublicAvatarList(): Awaited<ReturnType<AvatarC
69
69
  export declare function useViversePublicAvatarByID(id: string): Awaited<ReturnType<AvatarClient['getPublicAvatarByID']>> | undefined;
70
70
  export * from './material.js';
71
71
  export * as Vanilla from '@pmndrs/viverse';
72
+ export { EventAction, StateAction, BooleanOr, PointerLockRotateZoomActionBindings, KeyboardLocomotionActionBindings, updateSimpleCharacterVelocity, MoveRightAction, MoveLeftAction, MoveForwardAction, MoveBackwardAction, RunAction, JumpAction, ZoomAction, RotateYawAction, RotatePitchAction, shouldJump, lowerBody, upperBody, WalkAnimationUrl, RunAnimationUrl, IdleAnimationUrl, JumpUpAnimationUrl, JumpLoopAnimationUrl, JumpDownAnimationUrl, FirstPersonCharacterCameraBehavior, BvhCharacterPhysics, type BvhCharacterPhysicsOptions, BvhPhysicsWorld, bvhBoneMap, CharacterCameraBehavior, } from '@pmndrs/viverse';
73
+ export type { VRMHumanBoneName } from '@pmndrs/viverse';
72
74
  export * from '@viverse/sdk';
73
75
  export * from '@viverse/sdk/avatar-client';
74
76
  export * from './gamepad.js';
package/dist/index.js CHANGED
@@ -154,6 +154,8 @@ export function useViversePublicAvatarByID(id) {
154
154
  }
155
155
  export * from './material.js';
156
156
  export * as Vanilla from '@pmndrs/viverse';
157
+ // Direct re-exports from @pmndrs/viverse used in examples and docs
158
+ export { EventAction, StateAction, BooleanOr, PointerLockRotateZoomActionBindings, KeyboardLocomotionActionBindings, updateSimpleCharacterVelocity, MoveRightAction, MoveLeftAction, MoveForwardAction, MoveBackwardAction, RunAction, JumpAction, ZoomAction, RotateYawAction, RotatePitchAction, shouldJump, lowerBody, upperBody, WalkAnimationUrl, RunAnimationUrl, IdleAnimationUrl, JumpUpAnimationUrl, JumpLoopAnimationUrl, JumpDownAnimationUrl, FirstPersonCharacterCameraBehavior, BvhCharacterPhysics, BvhPhysicsWorld, bvhBoneMap, CharacterCameraBehavior, } from '@pmndrs/viverse';
157
159
  export * from '@viverse/sdk';
158
160
  export * from '@viverse/sdk/avatar-client';
159
161
  export * from './gamepad.js';
package/dist/simple.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { RunField, shouldJump, updateSimpleCharacterInputVelocity, updateSimpleCharacterRotation, } from '@pmndrs/viverse';
2
+ import { RunAction, shouldJump, updateSimpleCharacterVelocity, updateSimpleCharacterRotation, } from '@pmndrs/viverse';
3
+ import { IdleAnimationUrl, JumpDownAnimationUrl, JumpForwardAnimationUrl, JumpLoopAnimationUrl, JumpUpAnimationUrl, RunAnimationUrl, WalkAnimationUrl, } from '@pmndrs/viverse/src/animation/default.js';
3
4
  import { useFrame } from '@react-three/fiber';
4
5
  import { Graph, GrapthState, Parallel, RunTimeline, Switch, SwitchCase, timePassed } from '@react-three/timeline';
5
6
  import { forwardRef, Suspense, useImperativeHandle, useRef } from 'react';
@@ -8,20 +9,20 @@ import { CharacterAnimationAction } from './animation.js';
8
9
  import { useViverseActiveAvatar } from './index.js';
9
10
  import { CharacterModelProvider } from './model.js';
10
11
  import { useBvhCharacterPhysics } from './physics.js';
11
- import { useCharacterCameraBehavior, useCharacterModelLoader, useInputSystem } from './utils.js';
12
- export const SimpleCharacter = forwardRef(({ input, inputOptions, cameraBehavior, children, movement, physics: physicsOptions, model, useViverseAvatar, animation, ...props }, ref) => {
12
+ import { useCharacterCameraBehavior, useCharacterModelLoader, useSimpleCharacterActionBindings } from './utils.js';
13
+ export const SimpleCharacter = forwardRef(({ input, actionBindings, inputOptions, actionBindingOptions, cameraBehavior, children, movement, physics: physicsOptions, model, useViverseAvatar, animation, ...props }, ref) => {
14
+ useSimpleCharacterActionBindings(actionBindings ?? input, actionBindingOptions ?? inputOptions);
13
15
  const internalRef = useRef(null);
14
- const inputSystem = useInputSystem(input, inputOptions);
15
- useCharacterCameraBehavior(internalRef, inputSystem, cameraBehavior);
16
+ useCharacterCameraBehavior(internalRef, cameraBehavior);
16
17
  const physics = useBvhCharacterPhysics(internalRef, physicsOptions);
17
18
  const lastJumpTimeRef = useRef(-Infinity);
18
- useFrame((state) => updateSimpleCharacterInputVelocity(state.camera, inputSystem, physics, movement));
19
+ useFrame((state) => updateSimpleCharacterVelocity(state.camera, physics, movement));
19
20
  useFrame(() => {
20
21
  if (model != false || movement?.jump === false) {
21
22
  return;
22
23
  }
23
24
  const bufferTime = movement?.jump === true ? undefined : movement?.jump?.bufferTime;
24
- if (shouldJump(physics, inputSystem, lastJumpTimeRef.current, bufferTime)) {
25
+ if (shouldJump(physics, lastJumpTimeRef.current, bufferTime)) {
25
26
  lastJumpTimeRef.current = performance.now() / 1000;
26
27
  physics.applyVelocity(
27
28
  // eslint-disable-next-line @react-three/no-new-in-loop
@@ -29,9 +30,9 @@ export const SimpleCharacter = forwardRef(({ input, inputOptions, cameraBehavior
29
30
  }
30
31
  });
31
32
  useImperativeHandle(ref, () => internalRef.current, []);
32
- return (_jsx("group", { ...props, ref: internalRef, children: model == false ? (children) : (_jsx(SimpleCharacterModel, { animation: animation, inputSystem: inputSystem, physics: physics, model: model == true ? undefined : model, movement: movement, useViverseAvatar: useViverseAvatar, children: children })) }));
33
+ return (_jsx("group", { ...props, ref: internalRef, children: model == false ? (children) : (_jsx(SimpleCharacterModel, { animation: animation, physics: physics, model: model == true ? undefined : model, movement: movement, useViverseAvatar: useViverseAvatar, children: children })) }));
33
34
  });
34
- function SimpleCharacterModel({ children, model: modelOptions, movement, physics, useViverseAvatar = true, inputSystem, animation, }) {
35
+ function SimpleCharacterModel({ children, model: modelOptions, movement, physics, useViverseAvatar = true, animation, }) {
35
36
  const avatar = useViverseActiveAvatar();
36
37
  const model = useCharacterModelLoader(avatar != null && useViverseAvatar
37
38
  ? {
@@ -44,15 +45,15 @@ function SimpleCharacterModel({ children, model: modelOptions, movement, physics
44
45
  useFrame((state, delta) => updateSimpleCharacterRotation(delta, physics, state.camera, model, animation));
45
46
  return (_jsx(_Fragment, { children: _jsxs(CharacterModelProvider, { model: model, children: [_jsx(RunTimeline, { children: _jsxs(Graph, { enterState: "move", children: [_jsx(GrapthState, { name: "move", transitionTo: {
46
47
  jumpStart: {
47
- whenUpdate: () => shouldJump(physics, inputSystem, lastJumpTimeRef.current),
48
+ whenUpdate: () => shouldJump(physics, lastJumpTimeRef.current),
48
49
  },
49
50
  jumpLoop: { whenUpdate: () => !physics.isGrounded },
50
- }, children: _jsxs(Switch, { children: [_jsx(SwitchCase, { index: 0, condition: () => physics.inputVelocity.lengthSq() === 0, children: _jsx(Suspense, { fallback: null, children: _jsx(CharacterAnimationAction, { ...animation?.idle, fadeDuration: animation?.crossFadeDuration, url: { default: 'idle' } }) }) }), movement?.run != false && (_jsx(SwitchCase, { index: 1, condition: () => inputSystem.get(RunField), children: _jsx(Suspense, { fallback: null, children: _jsx(CharacterAnimationAction, { ...animation?.run, fadeDuration: animation?.crossFadeDuration, scaleTime: 0.8, url: { default: 'run' } }) }) })), movement?.walk != false && (_jsx(SwitchCase, { index: 2, children: _jsx(Suspense, { fallback: null, children: _jsx(CharacterAnimationAction, { ...animation?.walk, fadeDuration: animation?.crossFadeDuration, scaleTime: 0.5, url: { default: 'walk' } }) }) })), _jsx(SwitchCase, { index: 3, children: _jsx(Suspense, { fallback: null, children: _jsx(CharacterAnimationAction, { ...animation?.idle, fadeDuration: animation?.crossFadeDuration, url: { default: 'idle' } }) }) })] }) }), _jsx(GrapthState, { name: "jumpStart", transitionTo: {
51
+ }, children: _jsxs(Switch, { children: [_jsx(SwitchCase, { index: 0, condition: () => physics.inputVelocity.lengthSq() === 0, children: _jsx(Suspense, { fallback: null, children: _jsx(CharacterAnimationAction, { ...animation?.idle, fadeDuration: animation?.crossFadeDuration, url: IdleAnimationUrl }) }) }), movement?.run != false && (_jsx(SwitchCase, { index: 1, condition: () => RunAction.get(), children: _jsx(Suspense, { fallback: null, children: _jsx(CharacterAnimationAction, { ...animation?.run, fadeDuration: animation?.crossFadeDuration, scaleTime: 0.8, url: RunAnimationUrl }) }) })), movement?.walk != false && (_jsx(SwitchCase, { index: 2, children: _jsx(Suspense, { fallback: null, children: _jsx(CharacterAnimationAction, { ...animation?.walk, fadeDuration: animation?.crossFadeDuration, scaleTime: 0.5, url: WalkAnimationUrl }) }) })), _jsx(SwitchCase, { index: 3, children: _jsx(Suspense, { fallback: null, children: _jsx(CharacterAnimationAction, { ...animation?.idle, fadeDuration: animation?.crossFadeDuration, url: IdleAnimationUrl }) }) })] }) }), _jsx(GrapthState, { name: "jumpStart", transitionTo: {
51
52
  jumpDown: { whenUpdate: () => !physics.isGrounded },
52
- finally: () => (inputSystem.get(RunField) ? 'jumpForward' : 'jumpUp'),
53
- }, children: _jsx(Parallel, { type: "race", children: _jsxs(Suspense, { fallback: null, children: [_jsx(CharacterAnimationAction, { ...animation?.jumpUp, fadeDuration: animation?.crossFadeDuration ?? 0.1, until: () => timePassed(0.2, 'seconds'), update: () => void physics.inputVelocity.multiplyScalar(0.3), paused: true, url: { default: 'jumpUp' } }), _jsx(CharacterAnimationAction, { ...animation?.jumpForward, fadeDuration: animation?.crossFadeDuration ?? 0.1, paused: true, url: { default: 'jumpForward' } })] }) }) }), _jsx(GrapthState, { name: "jumpLoop", transitionTo: {
53
+ finally: () => (RunAction.get() ? 'jumpForward' : 'jumpUp'),
54
+ }, children: _jsx(Parallel, { type: "race", children: _jsxs(Suspense, { fallback: null, children: [_jsx(CharacterAnimationAction, { ...animation?.jumpUp, fadeDuration: animation?.crossFadeDuration ?? 0.1, until: () => timePassed(0.2, 'seconds'), update: () => void physics.inputVelocity.multiplyScalar(0.3), paused: true, url: JumpUpAnimationUrl }), _jsx(CharacterAnimationAction, { ...animation?.jumpForward, fadeDuration: animation?.crossFadeDuration ?? 0.1, paused: true, url: JumpForwardAnimationUrl })] }) }) }), _jsx(GrapthState, { name: "jumpLoop", transitionTo: {
54
55
  jumpDown: { whenUpdate: () => physics.isGrounded },
55
- }, children: _jsx(Suspense, { fallback: null, children: _jsx(CharacterAnimationAction, { ...animation?.jumpLoop, fadeDuration: animation?.crossFadeDuration, url: { default: 'jumpLoop' } }) }) }), _jsx(GrapthState, { name: "jumpUp", transitionTo: {
56
+ }, children: _jsx(Suspense, { fallback: null, children: _jsx(CharacterAnimationAction, { ...animation?.jumpLoop, fadeDuration: animation?.crossFadeDuration, url: JumpLoopAnimationUrl }) }) }), _jsx(GrapthState, { name: "jumpUp", transitionTo: {
56
57
  jumpDown: {
57
58
  whenUpdate: (_, _clock, actionTime) => actionTime > 0.3 && physics.isGrounded,
58
59
  },
@@ -60,8 +61,8 @@ function SimpleCharacterModel({ children, model: modelOptions, movement, physics
60
61
  }, children: _jsx(Suspense, { fallback: null, children: _jsx(CharacterAnimationAction, { ...animation?.jumpUp, fadeDuration: animation?.crossFadeDuration, loop: LoopOnce, init: () => {
61
62
  lastJumpTimeRef.current = performance.now() / 1000;
62
63
  physics.applyVelocity(new Vector3(0, (typeof movement?.jump === 'object' ? movement?.jump.speed : undefined) ?? 8, 0));
63
- }, url: { default: 'jumpUp' } }) }) }), _jsx(GrapthState, { name: "jumpForward", transitionTo: { finally: () => (physics.isGrounded ? 'move' : 'jumpLoop') }, children: _jsx(Suspense, { fallback: null, children: _jsx(CharacterAnimationAction, { ...animation?.jumpForward, fadeDuration: animation?.crossFadeDuration, scaleTime: 0.9, init: () => {
64
+ }, url: JumpUpAnimationUrl }) }) }), _jsx(GrapthState, { name: "jumpForward", transitionTo: { finally: () => (physics.isGrounded ? 'move' : 'jumpLoop') }, children: _jsx(Suspense, { fallback: null, children: _jsx(CharacterAnimationAction, { ...animation?.jumpForward, fadeDuration: animation?.crossFadeDuration, scaleTime: 0.9, init: () => {
64
65
  lastJumpTimeRef.current = performance.now() / 1000;
65
66
  physics.applyVelocity(new Vector3(0, 8, 0));
66
- }, loop: LoopOnce, url: { default: 'jumpForward' } }) }) }), _jsx(GrapthState, { name: "jumpDown", transitionTo: { finally: 'move' }, children: _jsx(Suspense, { fallback: null, children: _jsx(CharacterAnimationAction, { ...animation?.jumpDown, fadeDuration: animation?.crossFadeDuration, until: () => timePassed(150, 'milliseconds'), loop: LoopOnce, url: { default: 'jumpDown' } }) }) })] }) }), _jsx("primitive", { object: model.scene }), children] }) }));
67
+ }, loop: LoopOnce, url: JumpForwardAnimationUrl }) }) }), _jsx(GrapthState, { name: "jumpDown", transitionTo: { finally: 'move' }, children: _jsx(Suspense, { fallback: null, children: _jsx(CharacterAnimationAction, { ...animation?.jumpDown, fadeDuration: animation?.crossFadeDuration, until: () => timePassed(150, 'milliseconds'), loop: LoopOnce, url: JumpDownAnimationUrl }) }) })] }) }), _jsx("primitive", { object: model.scene }), children] }) }));
67
68
  }
package/dist/utils.d.ts CHANGED
@@ -1,9 +1,46 @@
1
- import { CharacterAnimationOptions, CharacterModelOptions, InputSystem, SimpleCharacterCameraBehaviorOptions, CharacterModel, Input } from '@pmndrs/viverse';
1
+ import { CharacterCameraBehavior, CharacterAnimationOptions, CharacterModelOptions, CharacterCameraBehaviorOptions, CharacterModel, SimpleCharacterActionBindingOptions, WriteonlyEventAction, StateAction } from '@pmndrs/viverse';
2
2
  import { RefObject } from 'react';
3
3
  import { Object3D } from 'three';
4
- export declare function useCharacterCameraBehavior(model: Object3D | RefObject<Object3D | null>, inputSystem: InputSystem, options?: SimpleCharacterCameraBehaviorOptions): void;
5
- export declare function useCharacterModelLoader(options?: CharacterModelOptions): CharacterModel;
4
+ export declare function useCharacterCameraBehavior(model: Object3D | RefObject<Object3D | null>, options?: CharacterCameraBehaviorOptions): RefObject<CharacterCameraBehavior | undefined>;
5
+ export declare function useCharacterModelLoader({ useViverseAvatar, ...modelOptions }?: CharacterModelOptions & {
6
+ useViverseAvatar?: boolean;
7
+ }): CharacterModel;
6
8
  export declare function useCharacterAnimationLoader(model: CharacterModel, options: CharacterAnimationOptions): import("three").AnimationClip;
7
- export declare function useInputSystem<T extends object>(inputs?: ReadonlyArray<Input<T> | {
8
- new (domElement: HTMLElement): Input<T>;
9
- }>, options?: T): InputSystem<T>;
9
+ /**
10
+ * @deprecated use the specific action binding hooks directly
11
+ */
12
+ export declare function useSimpleCharacterActionBindings(actionBindingsClasses?: ReadonlyArray<{
13
+ new (domElement: HTMLElement, abortSignal: AbortSignal): unknown;
14
+ }>, options?: SimpleCharacterActionBindingOptions): void;
15
+ export declare function useKeyboardActionBinding(action: WriteonlyEventAction<KeyboardEvent> | StateAction<boolean>, options: {
16
+ keys: Array<string>;
17
+ requiresPointerLock?: boolean;
18
+ }): void;
19
+ export declare function usePointerButtonActionBinding(action: WriteonlyEventAction<PointerEvent> | StateAction<boolean>, options: {
20
+ domElement?: HTMLElement | RefObject<HTMLElement | null>;
21
+ buttons?: Array<number>;
22
+ requiresPointerLock?: boolean;
23
+ }): void;
24
+ export declare function usePointerCaptureRotateZoomActionBindings(options?: {
25
+ rotationSpeed?: number;
26
+ zoomSpeed?: number;
27
+ }): void;
28
+ export declare function usePointerLockRotateZoomActionBindings(options?: {
29
+ rotationSpeed?: number;
30
+ zoomSpeed?: number;
31
+ lockOnClick?: boolean;
32
+ }): void;
33
+ export declare function useKeyboardLocomotionActionBindings(options?: {
34
+ moveForwardKeys?: Array<string>;
35
+ moveBackwardKeys?: Array<string>;
36
+ moveLeftKeys?: Array<string>;
37
+ moveRightKeys?: Array<string>;
38
+ runKeys?: Array<string>;
39
+ jumpKeys?: Array<string>;
40
+ requiresPointerLock?: boolean;
41
+ }): void;
42
+ export declare function useScreenButton(image: string): HTMLElement;
43
+ export declare function useScreenJoystickLocomotionActionBindings(options?: {
44
+ runDistancePx?: number;
45
+ deadZonePx?: number;
46
+ }): void;
package/dist/utils.js CHANGED
@@ -1,12 +1,21 @@
1
1
  import { VRM } from '@pixiv/three-vrm';
2
- import { CharacterCameraBehavior, flattenCharacterAnimationOptions, flattenCharacterModelOptions, InputSystem, loadCharacterModel, loadCharacterAnimation, ScreenJoystickInput, ScreenJumpButtonInput, PointerCaptureInput, LocomotionKeyboardInput, } from '@pmndrs/viverse';
2
+ import { CharacterCameraBehavior, flattenCharacterAnimationOptions, flattenCharacterModelOptions, loadCharacterModel, loadCharacterAnimation, ScreenJoystickLocomotionActionBindings, ScreenButtonJumpActionBindings, PointerCaptureRotateZoomActionBindings, KeyboardLocomotionActionBindings, applySimpleCharacterActionBindingOptions, KeyboardActionBinding, PointerButtonActionBinding, defaultScreenButtonStyles, PointerLockRotateZoomActionBindings, DefaultMoveBackwardKeys, DefaultMoveForwardKeys, DefaultJumpKeys, DefaultMoveLeftKeys, DefaultMoveRightKeys, DefaultRunKeys, } from '@pmndrs/viverse';
3
3
  import { useFrame, useThree } from '@react-three/fiber';
4
- import { useEffect, useMemo } from 'react';
4
+ import { useEffect, useMemo, useRef } from 'react';
5
5
  import { suspend } from 'suspend-react';
6
6
  import { Object3D } from 'three';
7
+ import { useViverseActiveAvatar } from './index.js';
7
8
  import { useBvhPhysicsWorld } from './physics.js';
8
- export function useCharacterCameraBehavior(model, inputSystem, options) {
9
- const behavior = useMemo(() => new CharacterCameraBehavior(), []);
9
+ export function useCharacterCameraBehavior(model, options) {
10
+ const behaviorRef = useRef(undefined);
11
+ useEffect(() => {
12
+ const behavior = new CharacterCameraBehavior();
13
+ behaviorRef.current = behavior;
14
+ return () => {
15
+ behaviorRef.current = undefined;
16
+ behavior.dispose();
17
+ };
18
+ }, []);
10
19
  const world = useBvhPhysicsWorld();
11
20
  const raycast = useMemo(() => world.raycast.bind(world), [world]);
12
21
  useFrame((state, delta) => {
@@ -14,12 +23,23 @@ export function useCharacterCameraBehavior(model, inputSystem, options) {
14
23
  if (resolvedModel == null) {
15
24
  return;
16
25
  }
17
- behavior.update(state.camera, resolvedModel, inputSystem, delta, raycast, options);
18
- });
26
+ behaviorRef.current?.update(state.camera, resolvedModel, delta, raycast, options);
27
+ }, -1);
28
+ return behaviorRef;
19
29
  }
20
30
  const loadCharacterModelSymbol = Symbol('loadCharacterModel');
21
- export function useCharacterModelLoader(options) {
22
- const model = suspend((_, ...params) => loadCharacterModel(...params), [loadCharacterModelSymbol, ...flattenCharacterModelOptions(options)]);
31
+ export function useCharacterModelLoader({ useViverseAvatar = true, ...modelOptions } = {}) {
32
+ const avatar = useViverseActiveAvatar();
33
+ const model = suspend((_, ...params) => loadCharacterModel(...params), [
34
+ loadCharacterModelSymbol,
35
+ ...flattenCharacterModelOptions(avatar != null && useViverseAvatar
36
+ ? {
37
+ ...modelOptions,
38
+ type: 'vrm',
39
+ url: avatar?.vrmUrl,
40
+ }
41
+ : modelOptions),
42
+ ]);
23
43
  useFrame((_, delta) => {
24
44
  if (model instanceof VRM) {
25
45
  model.update(delta);
@@ -32,36 +52,167 @@ const loadCharacterAnimationSymbol = Symbol('loadCharacterAnimation');
32
52
  export function useCharacterAnimationLoader(model, options) {
33
53
  return suspend((_, ...params) => loadCharacterAnimation(...params), [loadCharacterAnimationSymbol, model, ...flattenCharacterAnimationOptions(options)]);
34
54
  }
35
- export function useInputSystem(inputs = [
36
- ScreenJoystickInput,
37
- ScreenJumpButtonInput,
38
- PointerCaptureInput,
39
- LocomotionKeyboardInput,
40
- ], options = {}) {
55
+ /**
56
+ * @deprecated use the specific action binding hooks directly
57
+ */
58
+ export function useSimpleCharacterActionBindings(actionBindingsClasses = [
59
+ ScreenJoystickLocomotionActionBindings,
60
+ ScreenButtonJumpActionBindings,
61
+ PointerCaptureRotateZoomActionBindings,
62
+ KeyboardLocomotionActionBindings,
63
+ ], options) {
41
64
  const dom = useThree((s) => s.gl.domElement);
42
- const optionsRef = useMemo(() => ({}), []);
43
- //clearing and copying the options to optionsRef
44
- for (const key in optionsRef) {
45
- delete optionsRef[key];
46
- }
47
- Object.assign(optionsRef, options);
65
+ const actionBindingsList = useMemo(() => [],
48
66
  // eslint-disable-next-line react-hooks/exhaustive-deps
49
- const system = useMemo(() => new InputSystem(optionsRef), []);
67
+ [dom]);
50
68
  useEffect(() => {
51
- const removedInputs = new Set(system.inputs);
52
- for (const input of inputs) {
53
- const existingInput = system.inputs.find((existingInput) => typeof input === 'function' ? existingInput instanceof input.constructor : existingInput === input);
54
- if (existingInput != null) {
55
- removedInputs.delete(existingInput);
69
+ const removeActionBindingsSet = new Set(actionBindingsList);
70
+ for (const actionBindingsClass of actionBindingsClasses) {
71
+ const existingActionBindings = actionBindingsList.find((existingActionBindings) => existingActionBindings instanceof actionBindingsClass);
72
+ if (existingActionBindings != null) {
73
+ removeActionBindingsSet.delete(existingActionBindings);
56
74
  continue;
57
75
  }
58
- system.add(typeof input === 'function' ? new input(dom) : input);
76
+ const abortController = new AbortController();
77
+ actionBindingsList.push({ actionBindings: new actionBindingsClass(dom, abortController.signal), abortController });
78
+ }
79
+ for (const entry of removeActionBindingsSet) {
80
+ entry.abortController.abort();
81
+ const index = actionBindingsList.indexOf(entry);
82
+ if (index != -1) {
83
+ actionBindingsList.splice(index, 1);
84
+ }
85
+ }
86
+ applySimpleCharacterActionBindingOptions(actionBindingsList, options);
87
+ });
88
+ useEffect(() => () => {
89
+ actionBindingsList.forEach(({ abortController }) => abortController.abort());
90
+ actionBindingsList.length = 0;
91
+ }, [actionBindingsList]);
92
+ }
93
+ export function useKeyboardActionBinding(action, options) {
94
+ const ref = useRef(undefined);
95
+ const domElement = useThree((s) => s.gl.domElement);
96
+ useEffect(() => {
97
+ const abortController = new AbortController();
98
+ ref.current = new KeyboardActionBinding(action, domElement, abortController.signal);
99
+ return () => abortController.abort();
100
+ }, [action, domElement]);
101
+ useEffect(() => {
102
+ if (ref.current == null) {
103
+ return;
104
+ }
105
+ ref.current.keys = options.keys;
106
+ ref.current.requiresPointerLock = options.requiresPointerLock;
107
+ });
108
+ }
109
+ export function usePointerButtonActionBinding(action, options) {
110
+ const ref = useRef(undefined);
111
+ const canvasDomElement = useThree((s) => s.gl.domElement);
112
+ const domElement = options.domElement ?? canvasDomElement;
113
+ useEffect(() => {
114
+ const abortController = new AbortController();
115
+ ref.current = new PointerButtonActionBinding(action, domElement instanceof HTMLElement ? domElement : domElement.current, abortController.signal);
116
+ return () => abortController.abort();
117
+ }, [action, domElement]);
118
+ useEffect(() => {
119
+ if (ref.current == null) {
120
+ return;
121
+ }
122
+ ref.current.buttons = options.buttons;
123
+ ref.current.requiresPointerLock = options.requiresPointerLock;
124
+ });
125
+ }
126
+ export function usePointerCaptureRotateZoomActionBindings(options) {
127
+ const ref = useRef(undefined);
128
+ const domElement = useThree((s) => s.gl.domElement);
129
+ useEffect(() => {
130
+ const abortController = new AbortController();
131
+ ref.current = new PointerCaptureRotateZoomActionBindings(domElement, abortController.signal);
132
+ return () => abortController.abort();
133
+ }, [domElement]);
134
+ useEffect(() => {
135
+ if (ref.current == null) {
136
+ return;
59
137
  }
60
- for (const removedInput of removedInputs) {
61
- system.remove(removedInput);
62
- removedInput.dispose?.();
138
+ ref.current.rotationSpeed = options?.rotationSpeed;
139
+ ref.current.zoomSpeed = options?.zoomSpeed;
140
+ });
141
+ }
142
+ export function usePointerLockRotateZoomActionBindings(options) {
143
+ const ref = useRef(undefined);
144
+ const domElement = useThree((s) => s.gl.domElement);
145
+ useEffect(() => {
146
+ const abortController = new AbortController();
147
+ ref.current = new PointerLockRotateZoomActionBindings(domElement, abortController.signal);
148
+ return () => abortController.abort();
149
+ }, [domElement]);
150
+ useEffect(() => {
151
+ if (ref.current == null) {
152
+ return;
153
+ }
154
+ ref.current.lockOnClick = options?.lockOnClick;
155
+ ref.current.rotationSpeed = options?.rotationSpeed;
156
+ ref.current.zoomSpeed = options?.zoomSpeed;
157
+ });
158
+ }
159
+ export function useKeyboardLocomotionActionBindings(options) {
160
+ const ref = useRef(undefined);
161
+ const domElement = useThree((s) => s.gl.domElement);
162
+ useEffect(() => {
163
+ const abortController = new AbortController();
164
+ ref.current = new KeyboardLocomotionActionBindings(domElement, abortController.signal);
165
+ return () => abortController.abort();
166
+ }, [domElement]);
167
+ useEffect(() => {
168
+ if (ref.current == null) {
169
+ return;
170
+ }
171
+ ref.current.moveBackwardBinding.keys = options?.moveBackwardKeys ?? DefaultMoveBackwardKeys;
172
+ ref.current.moveBackwardBinding.requiresPointerLock = options?.requiresPointerLock;
173
+ ref.current.moveForwardBinding.keys = options?.moveForwardKeys ?? DefaultMoveForwardKeys;
174
+ ref.current.moveForwardBinding.requiresPointerLock = options?.requiresPointerLock;
175
+ ref.current.jumpBinding.keys = options?.jumpKeys ?? DefaultJumpKeys;
176
+ ref.current.jumpBinding.requiresPointerLock = options?.requiresPointerLock;
177
+ ref.current.moveLeftBinding.keys = options?.moveLeftKeys ?? DefaultMoveLeftKeys;
178
+ ref.current.moveLeftBinding.requiresPointerLock = options?.requiresPointerLock;
179
+ ref.current.moveRightBinding.keys = options?.moveRightKeys ?? DefaultMoveRightKeys;
180
+ ref.current.moveRightBinding.requiresPointerLock = options?.requiresPointerLock;
181
+ ref.current.runBinding.keys = options?.runKeys ?? DefaultRunKeys;
182
+ ref.current.runBinding.requiresPointerLock = options?.requiresPointerLock;
183
+ });
184
+ }
185
+ export function useScreenButton(image) {
186
+ const element = useMemo(() => document.createElement('div'), []);
187
+ element.style.backgroundImage = image;
188
+ const domElement = useThree((s) => s.gl.domElement);
189
+ useEffect(() => {
190
+ domElement.className = 'viverse-button viverse-jump mobile-only';
191
+ const parent = domElement.parentNode ?? domElement;
192
+ parent.appendChild(element);
193
+ Object.assign(element.style, defaultScreenButtonStyles);
194
+ const stopPropagation = (e) => e.stopPropagation();
195
+ element.addEventListener('pointerdown', stopPropagation);
196
+ return () => {
197
+ element.remove();
198
+ element.removeEventListener('pointerdown', stopPropagation);
199
+ };
200
+ }, [element, domElement]);
201
+ return element;
202
+ }
203
+ export function useScreenJoystickLocomotionActionBindings(options) {
204
+ const ref = useRef(undefined);
205
+ const domElement = useThree((s) => s.gl.domElement);
206
+ useEffect(() => {
207
+ const abortController = new AbortController();
208
+ ref.current = new ScreenJoystickLocomotionActionBindings(domElement, abortController.signal);
209
+ return () => abortController.abort();
210
+ }, [domElement]);
211
+ useEffect(() => {
212
+ if (ref.current == null) {
213
+ return;
63
214
  }
215
+ ref.current.deadZonePx = options?.deadZonePx;
216
+ ref.current.runDistancePx = options?.runDistancePx;
64
217
  });
65
- useEffect(() => () => system.inputs.forEach((input) => input.dispose?.()), [system]);
66
- return system;
67
218
  }
package/package.json CHANGED
@@ -24,12 +24,12 @@
24
24
  ],
25
25
  "dependencies": {
26
26
  "@viverse/sdk": "^1.2.10-alpha.0",
27
- "@react-three/timeline": "^0.3.5",
27
+ "@react-three/timeline": "^0.3.7",
28
28
  "suspend-react": "^0.1.3",
29
29
  "@pixiv/three-vrm": "^3.4.2",
30
30
  "zustand": "^5.0.6",
31
31
  "@react-three/xr": "^6.6.20",
32
- "@pmndrs/viverse": "^0.2.0"
32
+ "@pmndrs/viverse": "^0.2.2"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "@react-three/fiber": "*"
@@ -40,7 +40,7 @@
40
40
  "@types/react": "^19.1.8",
41
41
  "react": "^19.1.0"
42
42
  },
43
- "version": "0.2.0",
43
+ "version": "0.2.2",
44
44
  "scripts": {
45
45
  "build": "tsc",
46
46
  "check:prettier": "prettier --check src",