@jgengine/shell 0.18.0 → 0.19.0
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/CHANGELOG.md +265 -2
- package/dist/GamePlayerShell.js +27 -4
- package/dist/JoinGate.d.ts +17 -0
- package/dist/JoinGate.js +14 -0
- package/dist/Shell3dPresentation.d.ts +9 -8
- package/dist/Shell3dPresentation.js +57 -8
- package/dist/ShellChrome.d.ts +2 -0
- package/dist/ShellChrome.js +6 -0
- package/dist/ShellHudPresentation.d.ts +9 -1
- package/dist/ShellHudPresentation.js +21 -0
- package/dist/TerritoryOverlay.d.ts +21 -0
- package/dist/TerritoryOverlay.js +9 -0
- package/dist/audio/AudioComponents.js +13 -2
- package/dist/audio/audioEngine.d.ts +15 -1
- package/dist/audio/audioEngine.js +132 -11
- package/dist/audio/audioWire.d.ts +3 -0
- package/dist/audio/audioWire.js +7 -1
- package/dist/audio/loopParams.d.ts +39 -0
- package/dist/audio/loopParams.js +43 -0
- package/dist/camera/GameCameraRig.js +1 -1
- package/dist/camera/GameFirstPersonCamera.d.ts +4 -2
- package/dist/camera/GameFirstPersonCamera.js +52 -15
- package/dist/camera/Viewports.d.ts +34 -0
- package/dist/camera/Viewports.js +123 -0
- package/dist/camera/cameraRigs.js +77 -62
- package/dist/camera/orbitCameraMath.js +2 -2
- package/dist/camera/rigMath.d.ts +82 -4
- package/dist/camera/rigMath.js +133 -15
- package/dist/defineGame.d.ts +9 -0
- package/dist/defineGame.js +38 -6
- package/dist/devtools/DevtoolsOverlay.js +2 -0
- package/dist/devtools/KeysPanel.js +3 -3
- package/dist/devtools/agentBridge.d.ts +6 -0
- package/dist/devtools/agentBridge.js +15 -1
- package/dist/diagnostics/FpsCounter.d.ts +17 -0
- package/dist/diagnostics/FpsCounter.js +32 -0
- package/dist/drivers/FrameDriver.d.ts +5 -1
- package/dist/drivers/FrameDriver.js +74 -26
- package/dist/drivers/FrameRateLimiter.d.ts +9 -0
- package/dist/drivers/FrameRateLimiter.js +39 -0
- package/dist/drivers/HudOnlyDriver.js +10 -5
- package/dist/environment/DayNightSky.js +5 -5
- package/dist/environment/Daylight.d.ts +35 -1
- package/dist/environment/Daylight.js +98 -28
- package/dist/environment/EnvironmentScene.js +1 -1
- package/dist/environment/VolumetricClouds.js +20 -2
- package/dist/environment/daylightCycle.d.ts +20 -0
- package/dist/environment/daylightCycle.js +59 -7
- package/dist/environment/sunShadowMath.d.ts +45 -0
- package/dist/environment/sunShadowMath.js +50 -0
- package/dist/input/gamepadMerge.d.ts +10 -0
- package/dist/input/gamepadMerge.js +12 -0
- package/dist/input/gamepadPoll.d.ts +47 -0
- package/dist/input/gamepadPoll.js +142 -0
- package/dist/input/gamepadSource.d.ts +22 -0
- package/dist/input/gamepadSource.js +153 -0
- package/dist/input/mouseLook.js +2 -1
- package/dist/input/padHaptics.d.ts +17 -0
- package/dist/input/padHaptics.js +32 -0
- package/dist/input/pointerLock.d.ts +11 -0
- package/dist/input/pointerLock.js +19 -0
- package/dist/materialOverride.d.ts +4 -1
- package/dist/materialOverride.js +10 -1
- package/dist/postfx/PostProcessing.d.ts +11 -8
- package/dist/postfx/PostProcessing.js +38 -16
- package/dist/postfx/gradeShader.js +13 -0
- package/dist/postfx/stylizePass.d.ts +23 -0
- package/dist/postfx/stylizePass.js +216 -0
- package/dist/render/CascadedShadows.d.ts +11 -0
- package/dist/render/CascadedShadows.js +19 -8
- package/dist/render/EnvironmentLighting.d.ts +4 -1
- package/dist/render/EnvironmentLighting.js +45 -9
- package/dist/render/SceneLighting.d.ts +16 -2
- package/dist/render/SceneLighting.js +56 -6
- package/dist/render/SceneModels.js +45 -9
- package/dist/render/SkinnedInstances.d.ts +91 -0
- package/dist/render/SkinnedInstances.js +249 -0
- package/dist/render/TextureFiltering.d.ts +16 -0
- package/dist/render/TextureFiltering.js +64 -0
- package/dist/render/displayColor.d.ts +33 -0
- package/dist/render/displayColor.js +138 -0
- package/dist/render/modelLoad.d.ts +39 -0
- package/dist/render/modelLoad.js +89 -1
- package/dist/render/modelRender.d.ts +3 -0
- package/dist/render/modelRender.js +6 -2
- package/dist/render/resolveModel.js +4 -0
- package/dist/render/useFootIk.d.ts +58 -0
- package/dist/render/useFootIk.js +216 -0
- package/dist/render/useModelAnimation.d.ts +26 -0
- package/dist/render/useModelAnimation.js +184 -107
- package/dist/scene/AuthoredScene.d.ts +9 -0
- package/dist/scene/AuthoredScene.js +17 -1
- package/dist/scene/CityRenderer.js +666 -241
- package/dist/scene/cityWindowMaterial.d.ts +8 -6
- package/dist/scene/cityWindowMaterial.js +80 -21
- package/dist/settings/QuickControls.js +1 -1
- package/dist/settings/SettingsMenu.js +1 -1
- package/dist/settings/appliedSettings.d.ts +10 -2
- package/dist/settings/appliedSettings.js +9 -5
- package/dist/settings/settingsController.d.ts +3 -0
- package/dist/settings/settingsController.js +47 -3
- package/dist/structures/BuildingKitBatch.js +26 -22
- package/dist/structures/GeneratedBuilding.d.ts +3 -15
- package/dist/structures/GeneratedBuilding.js +21 -15
- package/dist/structures/buildingSurface.d.ts +21 -0
- package/dist/structures/buildingSurface.js +134 -0
- package/dist/terrain/terrainDetailMaterial.js +7 -3
- package/dist/useShellMultiplayerSync.d.ts +9 -2
- package/dist/useShellMultiplayerSync.js +30 -8
- package/dist/visibility/CullingProvider.d.ts +2 -1
- package/dist/visibility/CullingProvider.js +7 -2
- package/dist/world/GridWorldScene.js +4 -0
- package/dist/world/TileLayerRenderer.d.ts +9 -0
- package/dist/world/TileLayerRenderer.js +64 -0
- package/dist/world/WorldParticles.d.ts +31 -0
- package/dist/world/WorldParticles.js +128 -0
- package/dist/world/WorldScene.d.ts +2 -1
- package/dist/world/WorldScene.js +46 -4
- package/dist/world/entityPose.d.ts +4 -0
- package/dist/world/entityPose.js +9 -0
- package/dist/world/tileLayerInstances.d.ts +11 -0
- package/dist/world/tileLayerInstances.js +28 -0
- package/dist/worldSync.d.ts +5 -1
- package/dist/worldSync.js +25 -4
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +2 -2
- package/skills/game-design/SKILL.md +11 -1
- package/skills/jgengine/SKILL.md +9 -5
- package/skills/jgengine/api.md +205 -101
- package/skills/jgengine/capabilities.md +56 -0
- package/skills/jgengine/recipes/minimal-game.md +3 -0
- package/skills/jgengine-ui/SKILL.md +20 -15
- package/skills/jgengine-ui/api.md +538 -326
- package/skills/jgengine-ui/capabilities.md +37 -0
- package/skills/jgengine-ui/reference-primitives.md +89 -0
- package/skills/jgengine-ui/reference.md +2 -78
- package/skills/jgengine-world/SKILL.md +21 -3
- package/skills/jgengine-world/api.md +1172 -625
- package/skills/jgengine-world/capabilities.md +229 -3
- package/skills/jgengine-world/recipes/character-animation.md +39 -0
- package/skills/jgengine-world/recipes/controls.md +44 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +43 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
- package/skills/jgengine-world/reference.md +31 -2
- package/skills/jgengine-world/visibility.md +112 -0
- package/skills/level-design/SKILL.md +1 -1
- package/skills/jgengine-ui/references/visual-scorecard.md +0 -73
package/dist/audio/loopParams.js
CHANGED
|
@@ -26,3 +26,46 @@ export function clampLoopGain(gain) {
|
|
|
26
26
|
return 0;
|
|
27
27
|
return Math.min(1, Math.max(0, gain));
|
|
28
28
|
}
|
|
29
|
+
/** Lowest live filter cutoff, Hz. */
|
|
30
|
+
export const MIN_LOOP_CUTOFF = 10;
|
|
31
|
+
/** Highest live filter cutoff, Hz; the engine also caps it at the context's Nyquist frequency. */
|
|
32
|
+
export const MAX_LOOP_CUTOFF = 22050;
|
|
33
|
+
/**
|
|
34
|
+
* Clamp a loop filter cutoff into `MIN_LOOP_CUTOFF`–`min(MAX_LOOP_CUTOFF, nyquist)` Hz. Non-finite input
|
|
35
|
+
* falls back to `fallback` (the open, transparent end of that filter).
|
|
36
|
+
* @internal — the audio engine applies this; game code sets cutoffs through `ctx.game.audio.setLoop`.
|
|
37
|
+
*/
|
|
38
|
+
export function clampLoopCutoff(hz, fallback, nyquist = MAX_LOOP_CUTOFF) {
|
|
39
|
+
const max = Math.min(MAX_LOOP_CUTOFF, nyquist);
|
|
40
|
+
if (!Number.isFinite(hz))
|
|
41
|
+
return Math.min(max, Math.max(MIN_LOOP_CUTOFF, fallback));
|
|
42
|
+
return Math.min(max, Math.max(MIN_LOOP_CUTOFF, hz));
|
|
43
|
+
}
|
|
44
|
+
/** @internal */
|
|
45
|
+
export function createListenerVelocityTrack() {
|
|
46
|
+
return { last: null, velocity: { x: 0, y: 0, z: 0 } };
|
|
47
|
+
}
|
|
48
|
+
/** Camera jumps faster than this (world units/s) are cuts or teleports, not motion, and reset the estimate. */
|
|
49
|
+
const MAX_LISTENER_SPEED = 400;
|
|
50
|
+
/**
|
|
51
|
+
* Advance the listener velocity estimate by one frame: the position delta over `dt`, eased with a
|
|
52
|
+
* ~50 ms time constant so frame-time jitter does not warble doppler. A cut resets it to zero.
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
export function stepListenerVelocity(track, position, dt) {
|
|
56
|
+
const last = track.last;
|
|
57
|
+
track.last = { x: position.x, y: position.y, z: position.z };
|
|
58
|
+
if (last === null || !(dt > 0))
|
|
59
|
+
return track.velocity;
|
|
60
|
+
const vx = (position.x - last.x) / dt;
|
|
61
|
+
const vy = (position.y - last.y) / dt;
|
|
62
|
+
const vz = (position.z - last.z) / dt;
|
|
63
|
+
if (Math.hypot(vx, vy, vz) > MAX_LISTENER_SPEED) {
|
|
64
|
+
track.velocity = { x: 0, y: 0, z: 0 };
|
|
65
|
+
return track.velocity;
|
|
66
|
+
}
|
|
67
|
+
const blend = 1 - Math.exp(-dt / 0.05);
|
|
68
|
+
const v = track.velocity;
|
|
69
|
+
track.velocity = { x: v.x + (vx - v.x) * blend, y: v.y + (vy - v.y) * blend, z: v.z + (vz - v.z) * blend };
|
|
70
|
+
return track.velocity;
|
|
71
|
+
}
|
|
@@ -37,7 +37,7 @@ export function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerC
|
|
|
37
37
|
const kind = resolveRigKind(config);
|
|
38
38
|
switch (kind) {
|
|
39
39
|
case "first":
|
|
40
|
-
return (_jsx(GameFirstPersonCamera, { yawRef: yawRef, pitchRef: pitchRef, config: config?.firstPerson, followEntityId: followEntityId ?? undefined, viewmodel: viewmodel }));
|
|
40
|
+
return (_jsx(GameFirstPersonCamera, { yawRef: yawRef, pitchRef: pitchRef, config: config?.firstPerson, followEntityId: followEntityId ?? undefined, viewmodel: viewmodel, weapon: config?.weapon }));
|
|
41
41
|
case "topDown":
|
|
42
42
|
return _jsx(TopDownRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
|
|
43
43
|
case "rts":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ComponentType, type MutableRefObject } from "react";
|
|
2
2
|
import * as THREE from "three";
|
|
3
|
-
import type { FirstPersonCameraConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
import type { CameraWeaponView, FirstPersonCameraConfig } from "@jgengine/core/game/playableGame";
|
|
4
4
|
import type { EntityRenderCues } from "@jgengine/core/combat/renderCues";
|
|
5
5
|
/** World position of the first-person weapon muzzle, or false when no viewmodel is mounted. */
|
|
6
6
|
export declare function readFirstPersonMuzzle(target: THREE.Vector3): boolean;
|
|
@@ -15,5 +15,7 @@ export interface GameFirstPersonCameraProps {
|
|
|
15
15
|
followEntityId?: string;
|
|
16
16
|
/** Custom viewmodel component replacing the built-in three-mesh gun, rendered inside the same camera-locked, muzzle-tracked anchor. Ignored when `config.viewmodel === false`. */
|
|
17
17
|
viewmodel?: ComponentType<ViewmodelProps>;
|
|
18
|
+
/** Held-weapon source (`GameCameraConfig.weapon`): poses the viewmodel and adds recoil to the look. */
|
|
19
|
+
weapon?: (entityId: string) => CameraWeaponView | null;
|
|
18
20
|
}
|
|
19
|
-
export declare function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, viewmodel, }: GameFirstPersonCameraProps): import("react").JSX.Element | null;
|
|
21
|
+
export declare function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, viewmodel, weapon, }: GameFirstPersonCameraProps): import("react").JSX.Element | null;
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
3
|
-
import { useEffect, useRef } from "react";
|
|
3
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
4
4
|
import * as THREE from "three";
|
|
5
5
|
import { DEFAULT_EYE_HEIGHT } from "@jgengine/core/combat/shotOrigin";
|
|
6
|
+
import { createWeaponPresentation, viewmodelFovScale, } from "@jgengine/core/combat/weaponPresentation";
|
|
6
7
|
import { useGameContext } from "@jgengine/react/provider";
|
|
7
8
|
import { usePlayer } from "@jgengine/react/hooks";
|
|
8
9
|
import { useEntityRenderCues } from "../render/useEntityRenderCues.js";
|
|
9
10
|
import { usePlayerFov } from "./PlayerFov.js";
|
|
10
11
|
import { GAME_SIM_FRAME_PRIORITY, ORBIT_CAMERA_FRAME_PRIORITY } from "./orbitCameraMath.js";
|
|
12
|
+
import { requestRawPointerLock } from "../input/pointerLock.js";
|
|
11
13
|
const DEFAULT_SENSITIVITY = 0.0025;
|
|
12
14
|
const DEFAULT_MAX_PITCH = 1.45;
|
|
13
15
|
const VIEWMODEL_ORIGIN = new THREE.Vector3(0.34, -0.26, -0.72);
|
|
14
16
|
const MUZZLE_TIP_LOCAL = new THREE.Vector3(0, 0.03, -0.61);
|
|
15
|
-
const MUZZLE_OFFSET = VIEWMODEL_ORIGIN.clone().add(MUZZLE_TIP_LOCAL);
|
|
16
17
|
const muzzleWorld = new THREE.Vector3();
|
|
17
18
|
let muzzleTracked = false;
|
|
18
19
|
/** World position of the first-person weapon muzzle, or false when no viewmodel is mounted. */
|
|
@@ -22,7 +23,7 @@ export function readFirstPersonMuzzle(target) {
|
|
|
22
23
|
target.copy(muzzleWorld);
|
|
23
24
|
return true;
|
|
24
25
|
}
|
|
25
|
-
export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, viewmodel, }) {
|
|
26
|
+
export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, viewmodel, weapon, }) {
|
|
26
27
|
const eyeHeight = config?.eyeHeight ?? DEFAULT_EYE_HEIGHT;
|
|
27
28
|
const sensitivity = config?.sensitivity ?? DEFAULT_SENSITIVITY;
|
|
28
29
|
const maxPitch = config?.maxPitch ?? DEFAULT_MAX_PITCH;
|
|
@@ -34,12 +35,16 @@ export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId
|
|
|
34
35
|
const followId = followEntityId ?? userId;
|
|
35
36
|
const seededRef = useRef(false);
|
|
36
37
|
const cuesRef = useEntityRenderCues(followId);
|
|
38
|
+
const presentation = useMemo(() => createWeaponPresentation(), []);
|
|
39
|
+
const tuningRef = useRef(undefined);
|
|
40
|
+
const lastLookRef = useRef(null);
|
|
41
|
+
const poseRef = useRef(null);
|
|
37
42
|
useEffect(() => {
|
|
38
43
|
const requestLock = () => {
|
|
39
44
|
if (window.matchMedia?.("(pointer: coarse)").matches)
|
|
40
45
|
return;
|
|
41
46
|
if (document.pointerLockElement !== domElement)
|
|
42
|
-
|
|
47
|
+
requestRawPointerLock(domElement);
|
|
43
48
|
};
|
|
44
49
|
const onMove = (event) => {
|
|
45
50
|
if (document.pointerLockElement !== domElement)
|
|
@@ -54,7 +59,7 @@ export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId
|
|
|
54
59
|
window.removeEventListener("mousemove", onMove);
|
|
55
60
|
};
|
|
56
61
|
}, [domElement, sensitivity, maxPitch, yawRef, pitchRef]);
|
|
57
|
-
useFrame(() => {
|
|
62
|
+
useFrame((_, dt) => {
|
|
58
63
|
const entity = ctx.scene.entity.get(followId);
|
|
59
64
|
if (entity === null)
|
|
60
65
|
return;
|
|
@@ -62,22 +67,45 @@ export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId
|
|
|
62
67
|
seededRef.current = true;
|
|
63
68
|
yawRef.current = entity.rotationY;
|
|
64
69
|
}
|
|
65
|
-
const
|
|
70
|
+
const view = weapon?.(followId) ?? null;
|
|
71
|
+
const last = (lastLookRef.current ??= { yaw: yawRef.current, pitch: pitchRef.current });
|
|
72
|
+
const step = Math.max(1e-4, dt);
|
|
73
|
+
let pose = null;
|
|
74
|
+
if (view !== null) {
|
|
75
|
+
if (view.presentation !== tuningRef.current) {
|
|
76
|
+
tuningRef.current = view.presentation;
|
|
77
|
+
presentation.retune(view.presentation ?? {});
|
|
78
|
+
}
|
|
79
|
+
pose = presentation.update(dt, {
|
|
80
|
+
lookYawRate: (yawRef.current - last.yaw) / step,
|
|
81
|
+
lookPitchRate: (pitchRef.current - last.pitch) / step,
|
|
82
|
+
speed: cuesRef.current.speed,
|
|
83
|
+
bobPhase: cuesRef.current.bobPhase,
|
|
84
|
+
handling: view.handling,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
last.yaw = yawRef.current;
|
|
88
|
+
last.pitch = pitchRef.current;
|
|
89
|
+
poseRef.current = pose;
|
|
90
|
+
const yaw = yawRef.current - (pose?.lookYaw ?? 0);
|
|
91
|
+
const pitch = Math.max(-maxPitch, Math.min(maxPitch, pitchRef.current + (pose?.lookPitch ?? 0)));
|
|
92
|
+
const cosPitch = Math.cos(pitch);
|
|
66
93
|
camera.position.set(entity.position[0], entity.position[1] + eyeHeight, entity.position[2]);
|
|
67
|
-
camera.lookAt(camera.position.x + Math.sin(
|
|
94
|
+
camera.lookAt(camera.position.x + Math.sin(yaw) * cosPitch, camera.position.y + Math.sin(pitch), camera.position.z + Math.cos(yaw) * cosPitch);
|
|
68
95
|
if (camera.isPerspectiveCamera === true) {
|
|
69
96
|
const perspective = camera;
|
|
70
|
-
|
|
71
|
-
|
|
97
|
+
const fov = playerFov.fov * (pose?.fovScale ?? 1);
|
|
98
|
+
if (Math.abs(perspective.fov - fov) > 0.001) {
|
|
99
|
+
perspective.fov = fov;
|
|
72
100
|
perspective.updateProjectionMatrix();
|
|
73
101
|
}
|
|
74
102
|
}
|
|
75
103
|
}, ORBIT_CAMERA_FRAME_PRIORITY);
|
|
76
104
|
if (config?.viewmodel === false)
|
|
77
105
|
return null;
|
|
78
|
-
return _jsx(FirstPersonViewmodel, { camera: camera, viewmodel: viewmodel, cuesRef: cuesRef });
|
|
106
|
+
return _jsx(FirstPersonViewmodel, { camera: camera, viewmodel: viewmodel, cuesRef: cuesRef, poseRef: poseRef });
|
|
79
107
|
}
|
|
80
|
-
function FirstPersonViewmodel({ camera, viewmodel: Viewmodel, cuesRef, }) {
|
|
108
|
+
function FirstPersonViewmodel({ camera, viewmodel: Viewmodel, cuesRef, poseRef, }) {
|
|
81
109
|
const groupRef = useRef(null);
|
|
82
110
|
useEffect(() => () => {
|
|
83
111
|
muzzleTracked = false;
|
|
@@ -86,12 +114,21 @@ function FirstPersonViewmodel({ camera, viewmodel: Viewmodel, cuesRef, }) {
|
|
|
86
114
|
const group = groupRef.current;
|
|
87
115
|
if (group === null)
|
|
88
116
|
return;
|
|
117
|
+
const pose = poseRef.current;
|
|
118
|
+
const perspective = camera;
|
|
119
|
+
// Scaling camera-space x/y by this projects the viewmodel at its own FOV without a second render pass.
|
|
120
|
+
const squash = pose?.viewmodelFov == null || perspective.isPerspectiveCamera !== true ? 1 : viewmodelFovScale(perspective.fov, pose.viewmodelFov);
|
|
89
121
|
group.position.copy(camera.position);
|
|
90
122
|
group.quaternion.copy(camera.quaternion);
|
|
91
|
-
group.translateX(VIEWMODEL_ORIGIN.x);
|
|
92
|
-
group.translateY(VIEWMODEL_ORIGIN.y);
|
|
93
|
-
group.translateZ(VIEWMODEL_ORIGIN.z);
|
|
94
|
-
|
|
123
|
+
group.translateX((pose?.offset[0] ?? VIEWMODEL_ORIGIN.x) * squash);
|
|
124
|
+
group.translateY((pose?.offset[1] ?? VIEWMODEL_ORIGIN.y) * squash);
|
|
125
|
+
group.translateZ(pose?.offset[2] ?? VIEWMODEL_ORIGIN.z);
|
|
126
|
+
group.rotateX(pose?.pitch ?? 0);
|
|
127
|
+
group.rotateY(pose?.yaw ?? 0);
|
|
128
|
+
group.rotateZ(pose?.roll ?? 0);
|
|
129
|
+
group.scale.set(squash, squash, 1);
|
|
130
|
+
group.updateMatrixWorld();
|
|
131
|
+
group.localToWorld(muzzleWorld.copy(MUZZLE_TIP_LOCAL));
|
|
95
132
|
muzzleTracked = true;
|
|
96
133
|
}, GAME_SIM_FRAME_PRIORITY);
|
|
97
134
|
if (Viewmodel !== undefined) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { GameCameraConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
import { type ViewportDef, type ViewportsConfig } from "@jgengine/core/game/viewports";
|
|
4
|
+
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
5
|
+
import { type LocalPlayerSlot } from "@jgengine/core/runtime/localPlayers";
|
|
6
|
+
/** Camera yaw the split-screen renderer last used for a seat's user, so the walk controller moves that seat camera-relative. @internal */
|
|
7
|
+
export declare function seatCameraYaw(ctx: GameContext, userId: string): number | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* The viewports for the seats currently joined, re-resolved when a seat joins or leaves.
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare function useSeatViewports(ctx: GameContext, config: ViewportsConfig | undefined): readonly ViewportDef[];
|
|
13
|
+
/**
|
|
14
|
+
* Draws one scissored render per viewport. The primary seat keeps the main camera and its rig; every
|
|
15
|
+
* other seat gets a follow camera (chase or top-down) on its possessed entity. Mounted only while more
|
|
16
|
+
* than one viewport is drawn, since a render-priority frame callback takes over R3F's own render.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export declare function SplitScreenRenderer({ ctx, viewports, config, mainCamera, }: {
|
|
20
|
+
ctx: GameContext;
|
|
21
|
+
viewports: readonly ViewportDef[];
|
|
22
|
+
config: ViewportsConfig | undefined;
|
|
23
|
+
mainCamera: GameCameraConfig | undefined;
|
|
24
|
+
}): null;
|
|
25
|
+
/**
|
|
26
|
+
* One absolutely-positioned HUD root per viewport, laid over the canvas: render each seat's own
|
|
27
|
+
* health, prompts or score with `children(slot)`. Pass the same `viewports` config the game gave `defineGame`.
|
|
28
|
+
* @capability split-screen-hud Per-seat HUD roots positioned over each split-screen viewport.
|
|
29
|
+
*/
|
|
30
|
+
export declare function ViewportHuds({ ctx, config, children, }: {
|
|
31
|
+
ctx: GameContext;
|
|
32
|
+
config?: ViewportsConfig;
|
|
33
|
+
children: (slot: LocalPlayerSlot, viewport: ViewportDef) => ReactNode;
|
|
34
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
3
|
+
import { useMemo, useRef, useSyncExternalStore } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { resolveViewports, viewportPixels, } from "@jgengine/core/game/viewports";
|
|
6
|
+
import { localPlayers } from "@jgengine/core/runtime/localPlayers";
|
|
7
|
+
import { createChaseRigState, resolveChase, resolveTopDown, stepChase, topDownPose } from "./rigMath.js";
|
|
8
|
+
const SEAT_YAW = new WeakMap();
|
|
9
|
+
/** Camera yaw the split-screen renderer last used for a seat's user, so the walk controller moves that seat camera-relative. @internal */
|
|
10
|
+
export function seatCameraYaw(ctx, userId) {
|
|
11
|
+
return SEAT_YAW.get(ctx)?.get(userId);
|
|
12
|
+
}
|
|
13
|
+
function seatSlotIds(ctx) {
|
|
14
|
+
return localPlayers(ctx).slots().map((slot) => slot.slotId);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The viewports for the seats currently joined, re-resolved when a seat joins or leaves.
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export function useSeatViewports(ctx, config) {
|
|
21
|
+
const seats = localPlayers(ctx);
|
|
22
|
+
const key = useSyncExternalStore(seats.subscribe, () => seatSlotIds(ctx).join("|"), () => seatSlotIds(ctx).join("|"));
|
|
23
|
+
return useMemo(() => resolveViewports(config, key === "" ? [] : key.split("|")), [config, key]);
|
|
24
|
+
}
|
|
25
|
+
function seatRigKind(camera, main) {
|
|
26
|
+
return camera?.rig ?? (main?.rig === "topDown" ? "topDown" : "chase");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Draws one scissored render per viewport. The primary seat keeps the main camera and its rig; every
|
|
30
|
+
* other seat gets a follow camera (chase or top-down) on its possessed entity. Mounted only while more
|
|
31
|
+
* than one viewport is drawn, since a render-priority frame callback takes over R3F's own render.
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
export function SplitScreenRenderer({ ctx, viewports, config, mainCamera, }) {
|
|
35
|
+
const gl = useThree((state) => state.gl);
|
|
36
|
+
const scene = useThree((state) => state.scene);
|
|
37
|
+
const camera = useThree((state) => state.camera);
|
|
38
|
+
const size = useThree((state) => state.size);
|
|
39
|
+
const rigs = useRef(new Map());
|
|
40
|
+
useFrame((_state, dt) => {
|
|
41
|
+
const seats = localPlayers(ctx);
|
|
42
|
+
let yaws = SEAT_YAW.get(ctx);
|
|
43
|
+
if (yaws === undefined) {
|
|
44
|
+
yaws = new Map();
|
|
45
|
+
SEAT_YAW.set(ctx, yaws);
|
|
46
|
+
}
|
|
47
|
+
gl.setScissorTest(true);
|
|
48
|
+
for (const def of viewports) {
|
|
49
|
+
const slot = seats.slot(def.slot);
|
|
50
|
+
if (slot === null)
|
|
51
|
+
continue;
|
|
52
|
+
const pixels = viewportPixels(def.rect, size.width, size.height);
|
|
53
|
+
const view = slot.index === 0 ? camera : seatCamera(slot, def.camera ?? config?.camera, dt, yaws);
|
|
54
|
+
if (view instanceof THREE.PerspectiveCamera) {
|
|
55
|
+
view.aspect = pixels.width / pixels.height;
|
|
56
|
+
view.updateProjectionMatrix();
|
|
57
|
+
}
|
|
58
|
+
gl.setViewport(pixels.x, pixels.y, pixels.width, pixels.height);
|
|
59
|
+
gl.setScissor(pixels.x, pixels.y, pixels.width, pixels.height);
|
|
60
|
+
gl.render(scene, view);
|
|
61
|
+
}
|
|
62
|
+
gl.setScissorTest(false);
|
|
63
|
+
gl.setViewport(0, 0, size.width, size.height);
|
|
64
|
+
if (camera instanceof THREE.PerspectiveCamera) {
|
|
65
|
+
camera.aspect = size.width / size.height;
|
|
66
|
+
camera.updateProjectionMatrix();
|
|
67
|
+
}
|
|
68
|
+
}, 1);
|
|
69
|
+
function seatCamera(slot, cameraConfig, dt, yaws) {
|
|
70
|
+
let rig = rigs.current.get(slot.slotId);
|
|
71
|
+
if (rig === undefined) {
|
|
72
|
+
const created = new THREE.PerspectiveCamera();
|
|
73
|
+
if (camera instanceof THREE.PerspectiveCamera) {
|
|
74
|
+
created.near = camera.near;
|
|
75
|
+
created.far = camera.far;
|
|
76
|
+
}
|
|
77
|
+
rig = { camera: created, chase: createChaseRigState() };
|
|
78
|
+
rigs.current.set(slot.slotId, rig);
|
|
79
|
+
}
|
|
80
|
+
const entity = ctx.scene.entity.get(ctx.player.possession.active(slot.userId));
|
|
81
|
+
const follow = entity === null ? { x: 0, y: 0, z: 0 } : { x: entity.position[0], y: entity.position[1], z: entity.position[2] };
|
|
82
|
+
let pose;
|
|
83
|
+
if (seatRigKind(cameraConfig, mainCamera) === "topDown") {
|
|
84
|
+
const resolved = resolveTopDown(cameraConfig?.topDown ?? mainCamera?.topDown);
|
|
85
|
+
pose = topDownPose(follow, resolved, rig.camera.fov);
|
|
86
|
+
yaws.set(slot.userId, resolved.yaw);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const velocity = entity?.velocity;
|
|
90
|
+
const step = stepChase(rig.chase, {
|
|
91
|
+
follow,
|
|
92
|
+
yaw: entity?.rotationY ?? 0,
|
|
93
|
+
bodyPitch: entity?.rotationX ?? 0,
|
|
94
|
+
velocity: velocity === undefined ? null : { x: velocity[0], y: velocity[1], z: velocity[2] },
|
|
95
|
+
lookBack: false,
|
|
96
|
+
fovKick: 0,
|
|
97
|
+
}, resolveChase(cameraConfig?.chase ?? mainCamera?.chase), dt);
|
|
98
|
+
pose = step.pose;
|
|
99
|
+
yaws.set(slot.userId, step.anchorYaw);
|
|
100
|
+
}
|
|
101
|
+
rig.camera.position.set(pose.position.x, pose.position.y, pose.position.z);
|
|
102
|
+
rig.camera.fov = pose.fov;
|
|
103
|
+
rig.camera.lookAt(pose.lookAt.x, pose.lookAt.y, pose.lookAt.z);
|
|
104
|
+
return rig.camera;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* One absolutely-positioned HUD root per viewport, laid over the canvas: render each seat's own
|
|
110
|
+
* health, prompts or score with `children(slot)`. Pass the same `viewports` config the game gave `defineGame`.
|
|
111
|
+
* @capability split-screen-hud Per-seat HUD roots positioned over each split-screen viewport.
|
|
112
|
+
*/
|
|
113
|
+
export function ViewportHuds({ ctx, config, children, }) {
|
|
114
|
+
const viewports = useSeatViewports(ctx, config);
|
|
115
|
+
const seats = localPlayers(ctx);
|
|
116
|
+
return (_jsx(_Fragment, { children: viewports.map((viewport) => {
|
|
117
|
+
const slot = seats.slot(viewport.slot);
|
|
118
|
+
if (slot === null)
|
|
119
|
+
return null;
|
|
120
|
+
const [x, y, width, height] = viewport.rect;
|
|
121
|
+
return (_jsx("div", { "data-jg-viewport": viewport.slot, className: "pointer-events-none absolute overflow-hidden", style: { left: `${x * 100}%`, top: `${y * 100}%`, width: `${width * 100}%`, height: `${height * 100}%` }, children: children(slot, viewport) }, viewport.slot));
|
|
122
|
+
}) }));
|
|
123
|
+
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
2
2
|
import { useEffect, useMemo, useRef } from "react";
|
|
3
3
|
import { PerspectiveCamera, Quaternion, Vector3 } from "three";
|
|
4
|
+
import { snapPixelPerfectPosition } from "@jgengine/core/game/pixelPerfect";
|
|
4
5
|
import { usePlayer } from "@jgengine/react/hooks";
|
|
5
6
|
import { useGameContext } from "@jgengine/react/provider";
|
|
6
7
|
import { ORBIT_CAMERA_FRAME_PRIORITY } from "./orbitCameraMath.js";
|
|
7
|
-
import {
|
|
8
|
+
import { cinematicSample, clamp, clampBoomToHit, createChaseRigState, lockOnPose, observerPose, resolveChase, resolveObserver, resolveShoulder, resolveSideScroll, resolveSideScrollPose, resolveTopDown, rtsPanWorldDir, seatPose, shoulderPose, sideScrollFollowBlend, smoothYaw, stepChase, topDownPose, } from "./rigMath.js";
|
|
8
9
|
import { usePlayerFov } from "./PlayerFov.js";
|
|
9
10
|
import { useCameraShake } from "./shakeChannel.js";
|
|
10
11
|
import { applyCameraBlendStep, captureCameraBlendFrom, createCameraBlendScratch, } from "./cameraBlendMath.js";
|
|
12
|
+
import { requestRawPointerLock } from "../input/pointerLock.js";
|
|
11
13
|
export { applyCameraBlendStep, captureCameraBlendFrom, createCameraBlendScratch, } from "./cameraBlendMath.js";
|
|
12
14
|
export const CAMERA_RIG_FRAME_PRIORITY = ORBIT_CAMERA_FRAME_PRIORITY;
|
|
13
15
|
export const CAMERA_POST_FRAME_PRIORITY = ORBIT_CAMERA_FRAME_PRIORITY + 0.5;
|
|
@@ -162,6 +164,12 @@ export function SideScrollRig(props) {
|
|
|
162
164
|
};
|
|
163
165
|
followRef.current = follow;
|
|
164
166
|
const pose = resolveSideScrollPose(follow, resolved, config?.fov ?? currentFov(camera));
|
|
167
|
+
const pixelPerfect = props.config?.projection === "orthographic" ? props.config.pixelPerfect : undefined;
|
|
168
|
+
if (pixelPerfect !== undefined) {
|
|
169
|
+
const snapped = snapPixelPerfectPosition(pose.position, pixelPerfect.pixelsPerUnit);
|
|
170
|
+
pose.position = snapped;
|
|
171
|
+
pose.lookAt = snapPixelPerfectPosition(pose.lookAt, pixelPerfect.pixelsPerUnit);
|
|
172
|
+
}
|
|
165
173
|
commit(pose, dt);
|
|
166
174
|
}, CAMERA_RIG_FRAME_PRIORITY);
|
|
167
175
|
return null;
|
|
@@ -299,7 +307,7 @@ export function ShoulderRig(props) {
|
|
|
299
307
|
useEffect(() => {
|
|
300
308
|
const requestLock = () => {
|
|
301
309
|
if (document.pointerLockElement !== domElement)
|
|
302
|
-
|
|
310
|
+
requestRawPointerLock(domElement);
|
|
303
311
|
};
|
|
304
312
|
const onMove = (event) => {
|
|
305
313
|
if (document.pointerLockElement !== domElement)
|
|
@@ -338,8 +346,13 @@ export function ShoulderRig(props) {
|
|
|
338
346
|
useFrame((_, dt) => {
|
|
339
347
|
const sample = readFollow(ctx, followId);
|
|
340
348
|
const follow = sample?.pos ?? { x: 0, y: 0, z: 0 };
|
|
341
|
-
const
|
|
342
|
-
|
|
349
|
+
const weapon = followId === null ? null : (props.config?.weapon?.(followId) ?? null);
|
|
350
|
+
if (weapon === null) {
|
|
351
|
+
const adsTarget = aimingRef.current ? 1 : 0;
|
|
352
|
+
aimRef.current += (adsTarget - aimRef.current) * (1 - Math.exp(-(config?.adsTransitionSpeed ?? 10) * dt));
|
|
353
|
+
}
|
|
354
|
+
else
|
|
355
|
+
aimRef.current = weapon.handling.adsProgress;
|
|
343
356
|
const hip = resolveShoulder(config, false);
|
|
344
357
|
const ads = resolveShoulder(config, true);
|
|
345
358
|
const blended = {
|
|
@@ -348,7 +361,11 @@ export function ShoulderRig(props) {
|
|
|
348
361
|
distance: hip.distance + (ads.distance - hip.distance) * aimRef.current,
|
|
349
362
|
fov: hip.fov + (ads.fov - hip.fov) * aimRef.current,
|
|
350
363
|
};
|
|
351
|
-
const
|
|
364
|
+
const handling = weapon?.handling;
|
|
365
|
+
const yaw = props.yawRef.current - (handling === undefined ? 0 : handling.aimYaw + handling.cameraYaw);
|
|
366
|
+
// This rig's pitch is camera elevation, so a muzzle climb lowers it.
|
|
367
|
+
const pitch = props.pitchRef.current - (handling === undefined ? 0 : handling.aimPitch + handling.cameraPitch);
|
|
368
|
+
const pose = shoulderPose(follow, yaw, pitch, sideRef.current, blended);
|
|
352
369
|
commit(pose, dt);
|
|
353
370
|
}, CAMERA_RIG_FRAME_PRIORITY);
|
|
354
371
|
return null;
|
|
@@ -417,73 +434,71 @@ export function ChaseRig(props) {
|
|
|
417
434
|
const staticResolved = useMemo(() => resolveChase(config), [config]);
|
|
418
435
|
const { camera, commit, beginTransition } = useCameraCommit(props, followId);
|
|
419
436
|
const shake = useCameraShake();
|
|
420
|
-
const
|
|
421
|
-
const
|
|
422
|
-
const lastYawRef = useRef(null);
|
|
423
|
-
const anchorYawRef = useRef(null);
|
|
424
|
-
const rollRef = useRef(0);
|
|
425
|
-
const view = config?.view ?? "chase";
|
|
426
|
-
useEffect(beginTransition, [view]);
|
|
437
|
+
const stateRef = useRef(createChaseRigState());
|
|
438
|
+
const viewRef = useRef(null);
|
|
427
439
|
useFrame((_, dt) => {
|
|
428
440
|
const tuning = ctx.camera.chaseTuning();
|
|
429
|
-
const
|
|
430
|
-
const
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const
|
|
441
|
+
const merged = tuning === null ? config : { ...config, ...tuning };
|
|
442
|
+
const resolved = tuning === null ? staticResolved : resolveChase(merged);
|
|
443
|
+
const view = merged?.view ?? "chase";
|
|
444
|
+
if (viewRef.current !== view) {
|
|
445
|
+
beginTransition();
|
|
446
|
+
viewRef.current = view;
|
|
447
|
+
stateRef.current.offset = null;
|
|
448
|
+
}
|
|
449
|
+
const entity = followId === null ? null : ctx.scene.entity.get(followId);
|
|
450
|
+
const follow = entity === null ? { x: 0, y: 0, z: 0 } : { x: entity.position[0], y: entity.position[1], z: entity.position[2] };
|
|
451
|
+
const yaw = entity?.rotationY ?? 0;
|
|
452
|
+
const v = entity?.velocity;
|
|
453
|
+
const published = v !== undefined && (v[0] !== 0 || v[1] !== 0 || v[2] !== 0);
|
|
454
|
+
const lookBack = resolved.lookBackAction !== null && ctx.input.isDown(resolved.lookBackAction);
|
|
455
|
+
const clampBoom = resolved.collision
|
|
456
|
+
? (pivot, desired) => {
|
|
457
|
+
const dx = desired.x - pivot.x;
|
|
458
|
+
const dy = desired.y - pivot.y;
|
|
459
|
+
const dz = desired.z - pivot.z;
|
|
460
|
+
const length = Math.hypot(dx, dy, dz);
|
|
461
|
+
if (length <= 1e-6)
|
|
462
|
+
return desired;
|
|
463
|
+
const hit = ctx.scene.raycast({
|
|
464
|
+
origin: [pivot.x, pivot.y, pivot.z],
|
|
465
|
+
direction: [dx / length, dy / length, dz / length],
|
|
466
|
+
maxDistance: length + resolved.collisionRadius,
|
|
467
|
+
excludeInstanceIds: followId === null ? undefined : [followId],
|
|
468
|
+
filter: { entities: false },
|
|
469
|
+
});
|
|
470
|
+
return clampBoomToHit(pivot, desired, hit?.distance ?? null, resolved.collisionRadius, resolved.collisionMinDistance);
|
|
471
|
+
}
|
|
472
|
+
: undefined;
|
|
473
|
+
const step = stepChase(stateRef.current, {
|
|
474
|
+
follow,
|
|
475
|
+
yaw,
|
|
476
|
+
bodyPitch: entity?.rotationX ?? 0,
|
|
477
|
+
velocity: published ? { x: v[0], y: v[1], z: v[2] } : null,
|
|
478
|
+
lookBack: lookBack && view === "chase",
|
|
479
|
+
fovKick: ctx.camera.takeFovKick(),
|
|
480
|
+
}, resolved, dt, view === "chase" ? clampBoom : undefined);
|
|
481
|
+
props.yawRef.current = step.anchorYaw;
|
|
440
482
|
if (view !== "chase") {
|
|
441
483
|
const seat = view === "cockpit"
|
|
442
|
-
?
|
|
484
|
+
? merged?.seatOffsets?.cockpit ?? { x: 0, y: 1.2, z: 0.1 }
|
|
443
485
|
: view === "hood"
|
|
444
|
-
?
|
|
445
|
-
:
|
|
446
|
-
const
|
|
447
|
-
const pose = seatPose(follow,
|
|
448
|
-
if (
|
|
449
|
-
pose.position = seatPose(follow, yaw, seat, fov).position;
|
|
450
|
-
}
|
|
486
|
+
? merged?.seatOffsets?.hood ?? { x: 0, y: 1, z: 1.4 }
|
|
487
|
+
: merged?.seatOffsets?.rear ?? { x: 0, y: 1.6, z: -1.5 };
|
|
488
|
+
const seatLooksBack = view === "rear" || lookBack;
|
|
489
|
+
const pose = seatPose(follow, seatLooksBack ? yaw + Math.PI : yaw, seat, step.pose.fov);
|
|
490
|
+
if (seatLooksBack)
|
|
491
|
+
pose.position = seatPose(follow, yaw, seat, step.pose.fov).position;
|
|
451
492
|
props.yawRef.current = yaw;
|
|
452
493
|
commit(pose, dt);
|
|
453
494
|
return;
|
|
454
495
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
// yawResponse — so a strafe-flipped facing arcs the boom around the character instead of
|
|
458
|
-
// teleporting it to the far side. yawResponse: Infinity restores the legacy rigid follow.
|
|
459
|
-
let targetYaw = yaw;
|
|
460
|
-
let response = resolved.yawResponse;
|
|
461
|
-
if (resolved.velocityYawBlend > 0) {
|
|
462
|
-
const velocity = dt > 0
|
|
463
|
-
? { x: (follow.x - last.x) / dt, y: 0, z: (follow.z - last.z) / dt }
|
|
464
|
-
: { x: 0, y: 0, z: 0 };
|
|
465
|
-
targetYaw = velocityYawTarget(yaw, velocity, resolved);
|
|
466
|
-
response = resolved.velocityYawResponse;
|
|
496
|
+
if (resolved.shakePerSpeed > 0 && step.speed > 0) {
|
|
497
|
+
shake.shake(Math.min(resolved.shakePerSpeed * step.speed * dt, 0.1));
|
|
467
498
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
anchorYawRef.current = anchorYaw;
|
|
472
|
-
// Report camera yaw back to the shell like every other player-facing rig, so
|
|
473
|
-
// on-foot movement and aim stay camera-relative instead of frozen at yaw 0.
|
|
474
|
-
props.yawRef.current = anchorYaw;
|
|
475
|
-
const led = leadFollowPoint(follow, last, dt, resolved);
|
|
476
|
-
const desired = chaseDesiredPosition(led, anchorYaw, resolved);
|
|
477
|
-
const prev = posRef.current ?? desired;
|
|
478
|
-
const smoothed = springArmStep(prev, desired, resolved.springDamping, dt);
|
|
479
|
-
posRef.current = smoothed;
|
|
480
|
-
if (resolved.shakePerSpeed > 0 && speed > 0) {
|
|
481
|
-
shake.shake(Math.min(resolved.shakePerSpeed * speed * dt, 0.1));
|
|
482
|
-
}
|
|
483
|
-
const pose = { position: smoothed, lookAt: chaseLookAt(led, anchorYaw, resolved), fov };
|
|
484
|
-
commit(pose, dt);
|
|
485
|
-
if (rollRef.current !== 0)
|
|
486
|
-
camera.rotateZ(rollRef.current);
|
|
499
|
+
commit(step.pose, dt);
|
|
500
|
+
if (step.roll !== 0)
|
|
501
|
+
camera.rotateZ(step.roll);
|
|
487
502
|
}, CAMERA_RIG_FRAME_PRIORITY);
|
|
488
503
|
return null;
|
|
489
504
|
}
|
|
@@ -17,8 +17,8 @@ export const DEFAULT_ORBIT_CAMERA = {
|
|
|
17
17
|
minDistance: 4,
|
|
18
18
|
maxDistance: 28,
|
|
19
19
|
targetHeight: 1.2,
|
|
20
|
-
initialDistance:
|
|
21
|
-
initialHeight:
|
|
20
|
+
initialDistance: 7,
|
|
21
|
+
initialHeight: 2.4,
|
|
22
22
|
followLock: true,
|
|
23
23
|
followEnabled: true,
|
|
24
24
|
rotateSpeed: 0.22,
|