@jgengine/shell 0.18.1 → 0.21.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 +504 -7
- package/dist/GameHost.d.ts +12 -1
- package/dist/GameHost.js +14 -1
- package/dist/GamePlayerShell.js +132 -17
- package/dist/Shell3dPresentation.d.ts +5 -11
- package/dist/Shell3dPresentation.js +73 -11
- package/dist/ShellChrome.d.ts +2 -1
- package/dist/ShellChrome.js +22 -6
- package/dist/ShellHudPresentation.d.ts +20 -2
- package/dist/ShellHudPresentation.js +41 -6
- package/dist/audio/AudioComponents.js +6 -2
- package/dist/audio/audioEngine.d.ts +24 -0
- package/dist/audio/audioEngine.js +231 -83
- package/dist/audio/audioWire.d.ts +3 -0
- package/dist/audio/audioWire.js +9 -3
- package/dist/audio/loopParams.d.ts +39 -0
- package/dist/audio/loopParams.js +43 -0
- package/dist/audio/synthEngine.d.ts +7 -1
- package/dist/audio/synthEngine.js +103 -38
- package/dist/camera/GameCameraRig.js +3 -2
- package/dist/camera/GameFirstPersonCamera.d.ts +6 -4
- package/dist/camera/GameFirstPersonCamera.js +68 -27
- package/dist/camera/Viewports.d.ts +34 -0
- package/dist/camera/Viewports.js +123 -0
- package/dist/camera/cameraRigs.js +76 -62
- package/dist/camera/chaseHeading.d.ts +9 -0
- package/dist/camera/chaseHeading.js +10 -0
- package/dist/camera/customMuzzle.d.ts +14 -0
- package/dist/camera/customMuzzle.js +68 -0
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/orbitCameraMath.js +2 -2
- package/dist/camera/rigMath.d.ts +82 -4
- package/dist/camera/rigMath.js +133 -15
- package/dist/camera/rigResolve.d.ts +3 -0
- package/dist/camera/rigResolve.js +11 -0
- package/dist/combat/ProjectileModels.d.ts +31 -0
- package/dist/combat/ProjectileModels.js +70 -0
- package/dist/commandSink.d.ts +1 -2
- package/dist/commandSink.js +3 -2
- package/dist/defineGame.d.ts +19 -3
- package/dist/defineGame.js +39 -7
- package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
- package/dist/devtools/DevtoolsOverlay.js +15 -167
- package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
- package/dist/devtools/DevtoolsRuntime.js +61 -0
- package/dist/devtools/agentBridge.d.ts +6 -0
- package/dist/devtools/agentBridge.js +16 -2
- package/dist/devtools/devtoolsReports.d.ts +68 -0
- package/dist/devtools/devtoolsReports.js +100 -0
- package/dist/devtools/latencyInstrumentation.d.ts +3 -0
- package/dist/devtools/latencyInstrumentation.js +11 -0
- package/dist/devtools/playtestProbe.d.ts +4 -0
- package/dist/devtools/playtestProbe.js +22 -0
- package/dist/diagnostics/FpsCounter.d.ts +17 -0
- package/dist/diagnostics/FpsCounter.js +32 -0
- package/dist/drivers/FrameDriver.js +35 -20
- package/dist/drivers/FrameRateLimiter.d.ts +9 -0
- package/dist/drivers/FrameRateLimiter.js +39 -0
- package/dist/drivers/HudOnlyDriver.d.ts +6 -1
- package/dist/drivers/HudOnlyDriver.js +32 -14
- 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 +11 -10
- 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/gamepadPoll.d.ts +47 -0
- package/dist/input/gamepadPoll.js +142 -0
- package/dist/input/gamepadSource.d.ts +11 -1
- package/dist/input/gamepadSource.js +117 -71
- 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/inputSink.d.ts +6 -6
- package/dist/inputSink.js +60 -21
- package/dist/lazyShell3dPresentation.d.ts +9 -0
- package/dist/lazyShell3dPresentation.js +24 -0
- package/dist/materialOverride.d.ts +42 -0
- package/dist/materialOverride.js +95 -36
- package/dist/multiplayer.d.ts +33 -3
- package/dist/multiplayer.js +105 -22
- package/dist/pointer/pointerService.d.ts +2 -0
- package/dist/pointer/pointerService.js +12 -2
- package/dist/postfx/PostProcessing.d.ts +11 -1
- package/dist/postfx/PostProcessing.js +14 -42
- package/dist/postfx/gradeShader.d.ts +1 -1
- package/dist/postfx/gradeShader.js +30 -2
- package/dist/postfx/shadowPrepass.d.ts +3 -0
- package/dist/postfx/shadowPrepass.js +18 -0
- package/dist/postfx/stylizePass.d.ts +23 -0
- package/dist/postfx/stylizePass.js +216 -0
- package/dist/presentationRecovery.d.ts +75 -0
- package/dist/presentationRecovery.js +204 -0
- package/dist/presentationRecovery3d.d.ts +4 -0
- package/dist/presentationRecovery3d.js +32 -0
- package/dist/presentationResolve.d.ts +8 -1
- package/dist/presentationResolve.js +5 -2
- package/dist/render/CascadedShadows.d.ts +7 -2
- package/dist/render/CascadedShadows.js +98 -20
- package/dist/render/EnvironmentLighting.js +74 -35
- package/dist/render/HairCards.d.ts +7 -0
- package/dist/render/HairCards.js +23 -0
- package/dist/render/SceneModels.js +52 -78
- 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/authoredSurfaceMaterial.d.ts +43 -0
- package/dist/render/authoredSurfaceMaterial.js +108 -0
- package/dist/render/displayColor.d.ts +33 -0
- package/dist/render/displayColor.js +138 -0
- package/dist/render/materialAppearanceSignals.d.ts +23 -0
- package/dist/render/materialAppearanceSignals.js +58 -0
- package/dist/render/materialAsset.d.ts +98 -0
- package/dist/render/materialAsset.js +454 -0
- package/dist/render/measureBounds.js +16 -3
- package/dist/render/measureCollisionMesh.js +8 -1
- package/dist/render/modelAssets.d.ts +8 -0
- package/dist/render/modelAssets.js +43 -0
- package/dist/render/modelBindPose.d.ts +5 -0
- package/dist/render/modelBindPose.js +35 -0
- package/dist/render/modelLoad.d.ts +32 -0
- package/dist/render/modelLoad.js +70 -1
- package/dist/render/modelNodeVisibility.d.ts +6 -0
- package/dist/render/modelNodeVisibility.js +18 -0
- package/dist/render/modelRender.d.ts +18 -1
- package/dist/render/modelRender.js +81 -3
- package/dist/render/projectedOverdraw.d.ts +35 -0
- package/dist/render/projectedOverdraw.js +204 -0
- package/dist/render/resolveModel.d.ts +12 -2
- package/dist/render/resolveModel.js +25 -4
- package/dist/render/rigNode.d.ts +26 -0
- package/dist/render/rigNode.js +32 -0
- package/dist/render/spriteRender.d.ts +4 -0
- package/dist/render/spriteRender.js +5 -0
- package/dist/render/staticShapeInstances.d.ts +34 -0
- package/dist/render/staticShapeInstances.js +78 -0
- package/dist/render/useFootIk.d.ts +53 -4
- package/dist/render/useFootIk.js +262 -70
- package/dist/render/useModelAnimation.d.ts +50 -0
- package/dist/render/useModelAnimation.js +245 -99
- package/dist/render/useModelInstance.d.ts +25 -0
- package/dist/render/useModelInstance.js +43 -0
- package/dist/scatter/scatterModels.d.ts +3 -2
- package/dist/scatter/scatterModels.js +15 -7
- package/dist/scene/AuthoredScene.d.ts +23 -6
- package/dist/scene/AuthoredScene.js +46 -9
- package/dist/scene/CityRenderer.js +6 -6
- package/dist/scene/GeneratedAssetRenderer.js +11 -42
- package/dist/settings/QuickControls.js +1 -1
- package/dist/settings/SettingsMenu.js +11 -18
- package/dist/settings/SettingsRuntime.d.ts +5 -0
- package/dist/settings/SettingsRuntime.js +29 -2
- package/dist/settings/appliedSettings.d.ts +9 -3
- package/dist/settings/appliedSettings.js +6 -5
- package/dist/settings/settingsController.d.ts +3 -0
- package/dist/settings/settingsController.js +47 -3
- package/dist/shellControlSuspension.d.ts +18 -0
- package/dist/shellControlSuspension.js +28 -0
- package/dist/shellInputPublication.d.ts +28 -0
- package/dist/shellInputPublication.js +49 -0
- package/dist/shellPointerInput.d.ts +18 -0
- package/dist/shellPointerInput.js +49 -0
- package/dist/structures/BuildingKitBatch.d.ts +3 -1
- package/dist/structures/BuildingKitBatch.js +35 -56
- package/dist/structures/BuildingRenderBudget.d.ts +23 -0
- package/dist/structures/BuildingRenderBudget.js +54 -0
- package/dist/structures/GeneratedBuilding.d.ts +3 -15
- package/dist/structures/GeneratedBuilding.js +33 -22
- package/dist/structures/buildingKitMaterials.d.ts +20 -0
- package/dist/structures/buildingKitMaterials.js +70 -0
- package/dist/structures/buildingSpatialBatch.d.ts +11 -0
- package/dist/structures/buildingSpatialBatch.js +45 -0
- package/dist/structures/buildingSurface.d.ts +21 -0
- package/dist/structures/buildingSurface.js +134 -0
- package/dist/terrain/GrassField.d.ts +2 -1
- package/dist/terrain/GrassField.js +59 -5
- package/dist/terrain/grassGeometry.d.ts +10 -0
- package/dist/terrain/grassGeometry.js +55 -0
- package/dist/terrain/grassMaterial.js +2 -2
- package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
- package/dist/terrain/terrainDetailMaterial.js +7 -7
- package/dist/touch/TouchControlsOverlay.js +4 -4
- package/dist/useShellMultiplayerSync.d.ts +1 -1
- package/dist/useShellMultiplayerSync.js +9 -3
- package/dist/vfx/ParticleField.d.ts +23 -20
- package/dist/vfx/ParticleField.js +168 -50
- package/dist/visibility/CullingProvider.js +2 -62
- package/dist/visibility/cullingDriver.d.ts +9 -0
- package/dist/visibility/cullingDriver.js +68 -0
- package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
- package/dist/weather/AuthoredWeatherLayer.js +31 -0
- package/dist/weather/DustField.d.ts +2 -1
- package/dist/weather/DustField.js +17 -3
- package/dist/weather/FireSpreadLayer.d.ts +4 -2
- package/dist/weather/FireSpreadLayer.js +5 -11
- package/dist/weather/LightningStrike.d.ts +4 -1
- package/dist/weather/LightningStrike.js +13 -17
- package/dist/weather/RainField.d.ts +2 -1
- package/dist/weather/RainField.js +17 -3
- package/dist/weather/RainImpactField.d.ts +16 -0
- package/dist/weather/RainImpactField.js +89 -0
- package/dist/weather/SnowField.d.ts +2 -1
- package/dist/weather/SnowField.js +17 -3
- package/dist/weather/WeatherLayer.d.ts +11 -2
- package/dist/weather/WeatherLayer.js +30 -6
- package/dist/weather/index.d.ts +2 -0
- package/dist/weather/index.js +2 -0
- package/dist/weather/weatherMath.d.ts +2 -0
- package/dist/weather/weatherMath.js +8 -0
- package/dist/weather/weatherShelter.d.ts +18 -0
- package/dist/weather/weatherShelter.js +75 -0
- package/dist/weather/weatherUniforms.d.ts +40 -1
- package/dist/weather/weatherUniforms.js +29 -9
- package/dist/world/AuthoredFlocks.d.ts +22 -0
- package/dist/world/AuthoredFlocks.js +87 -0
- package/dist/world/StaticObjectModels.d.ts +8 -0
- package/dist/world/StaticObjectModels.js +75 -0
- package/dist/world/WorldHud.d.ts +14 -6
- package/dist/world/WorldHud.js +25 -12
- package/dist/world/WorldParticles.d.ts +10 -23
- package/dist/world/WorldParticles.js +210 -62
- package/dist/world/WorldScene.d.ts +3 -1
- package/dist/world/WorldScene.js +41 -26
- package/dist/world/staticObjectBatches.d.ts +35 -0
- package/dist/world/staticObjectBatches.js +163 -0
- package/dist/world/worldBarSamples.d.ts +37 -33
- package/dist/world/worldBarSamples.js +117 -75
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +2 -2
- package/skills/game-design/SKILL.md +2 -2
- package/skills/jgengine/SKILL.md +28 -6
- package/skills/jgengine/api.md +139 -99
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine/recipes/minimal-game.md +10 -2
- package/skills/jgengine-ui/SKILL.md +16 -17
- package/skills/jgengine-ui/api.md +603 -365
- package/skills/jgengine-ui/capabilities.md +131 -1
- package/skills/jgengine-ui/reference-primitives.md +179 -0
- package/skills/jgengine-ui/reference.md +4 -81
- package/skills/jgengine-world/SKILL.md +25 -4
- package/skills/jgengine-world/api.md +1045 -655
- package/skills/jgengine-world/capabilities.md +246 -9
- package/skills/jgengine-world/recipes/character-animation.md +62 -0
- package/skills/jgengine-world/recipes/controls.md +48 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +55 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
- package/skills/jgengine-world/reference.md +180 -4
- package/skills/jgengine-world/visibility.md +139 -0
- package/skills/level-design/SKILL.md +1 -1
- package/dist/render/testFixtures/proceduralSpriteAtlas.d.ts +0 -15
- package/dist/render/testFixtures/proceduralSpriteAtlas.js +0 -30
- package/dist/render/testFixtures/proceduralTileLayer.d.ts +0 -13
- package/dist/render/testFixtures/proceduralTileLayer.js +0 -30
|
@@ -5,10 +5,12 @@ import { snapPixelPerfectPosition } from "@jgengine/core/game/pixelPerfect";
|
|
|
5
5
|
import { usePlayer } from "@jgengine/react/hooks";
|
|
6
6
|
import { useGameContext } from "@jgengine/react/provider";
|
|
7
7
|
import { ORBIT_CAMERA_FRAME_PRIORITY } from "./orbitCameraMath.js";
|
|
8
|
-
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";
|
|
9
9
|
import { usePlayerFov } from "./PlayerFov.js";
|
|
10
10
|
import { useCameraShake } from "./shakeChannel.js";
|
|
11
11
|
import { applyCameraBlendStep, captureCameraBlendFrom, createCameraBlendScratch, } from "./cameraBlendMath.js";
|
|
12
|
+
import { requestRawPointerLock } from "../input/pointerLock.js";
|
|
13
|
+
import { resolveChaseHeading } from "./chaseHeading.js";
|
|
12
14
|
export { applyCameraBlendStep, captureCameraBlendFrom, createCameraBlendScratch, } from "./cameraBlendMath.js";
|
|
13
15
|
export const CAMERA_RIG_FRAME_PRIORITY = ORBIT_CAMERA_FRAME_PRIORITY;
|
|
14
16
|
export const CAMERA_POST_FRAME_PRIORITY = ORBIT_CAMERA_FRAME_PRIORITY + 0.5;
|
|
@@ -306,7 +308,7 @@ export function ShoulderRig(props) {
|
|
|
306
308
|
useEffect(() => {
|
|
307
309
|
const requestLock = () => {
|
|
308
310
|
if (document.pointerLockElement !== domElement)
|
|
309
|
-
|
|
311
|
+
requestRawPointerLock(domElement);
|
|
310
312
|
};
|
|
311
313
|
const onMove = (event) => {
|
|
312
314
|
if (document.pointerLockElement !== domElement)
|
|
@@ -345,8 +347,13 @@ export function ShoulderRig(props) {
|
|
|
345
347
|
useFrame((_, dt) => {
|
|
346
348
|
const sample = readFollow(ctx, followId);
|
|
347
349
|
const follow = sample?.pos ?? { x: 0, y: 0, z: 0 };
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
+
const weapon = followId === null ? null : (props.config?.weapon?.(followId) ?? null);
|
|
351
|
+
if (weapon === null) {
|
|
352
|
+
const adsTarget = aimingRef.current ? 1 : 0;
|
|
353
|
+
aimRef.current += (adsTarget - aimRef.current) * (1 - Math.exp(-(config?.adsTransitionSpeed ?? 10) * dt));
|
|
354
|
+
}
|
|
355
|
+
else
|
|
356
|
+
aimRef.current = weapon.handling.adsProgress;
|
|
350
357
|
const hip = resolveShoulder(config, false);
|
|
351
358
|
const ads = resolveShoulder(config, true);
|
|
352
359
|
const blended = {
|
|
@@ -355,7 +362,11 @@ export function ShoulderRig(props) {
|
|
|
355
362
|
distance: hip.distance + (ads.distance - hip.distance) * aimRef.current,
|
|
356
363
|
fov: hip.fov + (ads.fov - hip.fov) * aimRef.current,
|
|
357
364
|
};
|
|
358
|
-
const
|
|
365
|
+
const handling = weapon?.handling;
|
|
366
|
+
const yaw = props.yawRef.current - (handling === undefined ? 0 : handling.aimYaw + handling.cameraYaw);
|
|
367
|
+
// This rig's pitch is camera elevation, so a muzzle climb lowers it.
|
|
368
|
+
const pitch = props.pitchRef.current - (handling === undefined ? 0 : handling.aimPitch + handling.cameraPitch);
|
|
369
|
+
const pose = shoulderPose(follow, yaw, pitch, sideRef.current, blended);
|
|
359
370
|
commit(pose, dt);
|
|
360
371
|
}, CAMERA_RIG_FRAME_PRIORITY);
|
|
361
372
|
return null;
|
|
@@ -424,73 +435,76 @@ export function ChaseRig(props) {
|
|
|
424
435
|
const staticResolved = useMemo(() => resolveChase(config), [config]);
|
|
425
436
|
const { camera, commit, beginTransition } = useCameraCommit(props, followId);
|
|
426
437
|
const shake = useCameraShake();
|
|
427
|
-
const
|
|
428
|
-
const
|
|
429
|
-
const
|
|
430
|
-
const anchorYawRef = useRef(null);
|
|
431
|
-
const rollRef = useRef(0);
|
|
432
|
-
const view = config?.view ?? "chase";
|
|
433
|
-
useEffect(beginTransition, [view]);
|
|
438
|
+
const stateRef = useRef(createChaseRigState());
|
|
439
|
+
const viewRef = useRef(null);
|
|
440
|
+
const inputSeededRef = useRef(false);
|
|
434
441
|
useFrame((_, dt) => {
|
|
435
442
|
const tuning = ctx.camera.chaseTuning();
|
|
436
|
-
const
|
|
437
|
-
const
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
443
|
+
const merged = tuning === null ? config : { ...config, ...tuning };
|
|
444
|
+
const resolved = tuning === null ? staticResolved : resolveChase(merged);
|
|
445
|
+
const view = merged?.view ?? "chase";
|
|
446
|
+
if (viewRef.current !== view) {
|
|
447
|
+
beginTransition();
|
|
448
|
+
viewRef.current = view;
|
|
449
|
+
stateRef.current.offset = null;
|
|
450
|
+
}
|
|
451
|
+
const entity = followId === null ? null : ctx.scene.entity.get(followId);
|
|
452
|
+
const follow = entity === null ? { x: 0, y: 0, z: 0 } : { x: entity.position[0], y: entity.position[1], z: entity.position[2] };
|
|
453
|
+
if (!inputSeededRef.current && merged?.headingSource === "input" && view === "chase" && (entity !== null || props.config?.initialYaw !== undefined)) {
|
|
454
|
+
props.yawRef.current = resolveChaseHeading(merged, entity?.rotationY ?? 0, undefined, props.config?.initialYaw);
|
|
455
|
+
inputSeededRef.current = true;
|
|
456
|
+
}
|
|
457
|
+
const yaw = resolveChaseHeading(merged, entity?.rotationY ?? 0, props.yawRef.current);
|
|
458
|
+
const v = entity?.velocity;
|
|
459
|
+
const published = v !== undefined && (v[0] !== 0 || v[1] !== 0 || v[2] !== 0);
|
|
460
|
+
const lookBack = resolved.lookBackAction !== null && ctx.input.isDown(resolved.lookBackAction);
|
|
461
|
+
const clampBoom = resolved.collision
|
|
462
|
+
? (pivot, desired) => {
|
|
463
|
+
const dx = desired.x - pivot.x;
|
|
464
|
+
const dy = desired.y - pivot.y;
|
|
465
|
+
const dz = desired.z - pivot.z;
|
|
466
|
+
const length = Math.hypot(dx, dy, dz);
|
|
467
|
+
if (length <= 1e-6)
|
|
468
|
+
return desired;
|
|
469
|
+
const hit = ctx.scene.raycast({
|
|
470
|
+
origin: [pivot.x, pivot.y, pivot.z],
|
|
471
|
+
direction: [dx / length, dy / length, dz / length],
|
|
472
|
+
maxDistance: length + resolved.collisionRadius,
|
|
473
|
+
excludeInstanceIds: followId === null ? undefined : [followId],
|
|
474
|
+
filter: { entities: false },
|
|
475
|
+
});
|
|
476
|
+
return clampBoomToHit(pivot, desired, hit?.distance ?? null, resolved.collisionRadius, resolved.collisionMinDistance);
|
|
477
|
+
}
|
|
478
|
+
: undefined;
|
|
479
|
+
const step = stepChase(stateRef.current, {
|
|
480
|
+
follow,
|
|
481
|
+
yaw,
|
|
482
|
+
bodyPitch: entity?.rotationX ?? 0,
|
|
483
|
+
velocity: published ? { x: v[0], y: v[1], z: v[2] } : null,
|
|
484
|
+
lookBack: lookBack && view === "chase",
|
|
485
|
+
fovKick: ctx.camera.takeFovKick(),
|
|
486
|
+
}, resolved, dt, view === "chase" ? clampBoom : undefined);
|
|
487
|
+
props.yawRef.current = resolveChaseHeading(merged, step.anchorYaw, props.yawRef.current);
|
|
447
488
|
if (view !== "chase") {
|
|
448
489
|
const seat = view === "cockpit"
|
|
449
|
-
?
|
|
490
|
+
? merged?.seatOffsets?.cockpit ?? { x: 0, y: 1.2, z: 0.1 }
|
|
450
491
|
: view === "hood"
|
|
451
|
-
?
|
|
452
|
-
:
|
|
453
|
-
const
|
|
454
|
-
const pose = seatPose(follow,
|
|
455
|
-
if (
|
|
456
|
-
pose.position = seatPose(follow, yaw, seat, fov).position;
|
|
457
|
-
}
|
|
492
|
+
? merged?.seatOffsets?.hood ?? { x: 0, y: 1, z: 1.4 }
|
|
493
|
+
: merged?.seatOffsets?.rear ?? { x: 0, y: 1.6, z: -1.5 };
|
|
494
|
+
const seatLooksBack = view === "rear" || lookBack;
|
|
495
|
+
const pose = seatPose(follow, seatLooksBack ? yaw + Math.PI : yaw, seat, step.pose.fov);
|
|
496
|
+
if (seatLooksBack)
|
|
497
|
+
pose.position = seatPose(follow, yaw, seat, step.pose.fov).position;
|
|
458
498
|
props.yawRef.current = yaw;
|
|
459
499
|
commit(pose, dt);
|
|
460
500
|
return;
|
|
461
501
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
// yawResponse — so a strafe-flipped facing arcs the boom around the character instead of
|
|
465
|
-
// teleporting it to the far side. yawResponse: Infinity restores the legacy rigid follow.
|
|
466
|
-
let targetYaw = yaw;
|
|
467
|
-
let response = resolved.yawResponse;
|
|
468
|
-
if (resolved.velocityYawBlend > 0) {
|
|
469
|
-
const velocity = dt > 0
|
|
470
|
-
? { x: (follow.x - last.x) / dt, y: 0, z: (follow.z - last.z) / dt }
|
|
471
|
-
: { x: 0, y: 0, z: 0 };
|
|
472
|
-
targetYaw = velocityYawTarget(yaw, velocity, resolved);
|
|
473
|
-
response = resolved.velocityYawResponse;
|
|
502
|
+
if (resolved.shakePerSpeed > 0 && step.speed > 0) {
|
|
503
|
+
shake.shake(Math.min(resolved.shakePerSpeed * step.speed * dt, 0.1));
|
|
474
504
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
anchorYawRef.current = anchorYaw;
|
|
479
|
-
// Report camera yaw back to the shell like every other player-facing rig, so
|
|
480
|
-
// on-foot movement and aim stay camera-relative instead of frozen at yaw 0.
|
|
481
|
-
props.yawRef.current = anchorYaw;
|
|
482
|
-
const led = leadFollowPoint(follow, last, dt, resolved);
|
|
483
|
-
const desired = chaseDesiredPosition(led, anchorYaw, resolved);
|
|
484
|
-
const prev = posRef.current ?? desired;
|
|
485
|
-
const smoothed = springArmStep(prev, desired, resolved.springDamping, dt);
|
|
486
|
-
posRef.current = smoothed;
|
|
487
|
-
if (resolved.shakePerSpeed > 0 && speed > 0) {
|
|
488
|
-
shake.shake(Math.min(resolved.shakePerSpeed * speed * dt, 0.1));
|
|
489
|
-
}
|
|
490
|
-
const pose = { position: smoothed, lookAt: chaseLookAt(led, anchorYaw, resolved), fov };
|
|
491
|
-
commit(pose, dt);
|
|
492
|
-
if (rollRef.current !== 0)
|
|
493
|
-
camera.rotateZ(rollRef.current);
|
|
505
|
+
commit(step.pose, dt);
|
|
506
|
+
if (step.roll !== 0)
|
|
507
|
+
camera.rotateZ(step.roll);
|
|
494
508
|
}, CAMERA_RIG_FRAME_PRIORITY);
|
|
495
509
|
return null;
|
|
496
510
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ChaseCameraConfig } from "@jgengine/core/game/playableGame";
|
|
2
|
+
/**
|
|
3
|
+
* Select a chase heading without feeding target facing or camera smoothing back into input-owned yaw.
|
|
4
|
+
* An undefined input yaw seeds from `initialYaw`, or the acquired target's body heading.
|
|
5
|
+
* Seat views always use body heading; omitting `headingSource` preserves body follow.
|
|
6
|
+
*
|
|
7
|
+
* @capability chase-heading keep camera-relative input heading independent of body facing while retaining vehicle and seat camera follow
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveChaseHeading(config: Pick<ChaseCameraConfig, "headingSource" | "view"> | undefined, bodyYaw: number, inputYaw: number | undefined, initialYaw?: number): number;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Select a chase heading without feeding target facing or camera smoothing back into input-owned yaw.
|
|
3
|
+
* An undefined input yaw seeds from `initialYaw`, or the acquired target's body heading.
|
|
4
|
+
* Seat views always use body heading; omitting `headingSource` preserves body follow.
|
|
5
|
+
*
|
|
6
|
+
* @capability chase-heading keep camera-relative input heading independent of body facing while retaining vehicle and seat camera follow
|
|
7
|
+
*/
|
|
8
|
+
export function resolveChaseHeading(config, bodyYaw, inputYaw, initialYaw) {
|
|
9
|
+
return config?.headingSource === "input" && (config.view ?? "chase") === "chase" ? inputYaw ?? initialYaw ?? bodyYaw : bodyYaw;
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Camera, Vector3 } from "three";
|
|
2
|
+
/** Writes a live presentation muzzle in world space; false means this rig has no muzzle available. */
|
|
3
|
+
export type FirstPersonMuzzleReader = (target: Vector3) => boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Register a custom rig's presentation muzzle for its Three.js camera. Newest available custom
|
|
6
|
+
* reader wins over that camera's stock muzzle; call the returned cleanup on unmount.
|
|
7
|
+
*
|
|
8
|
+
* @capability custom-viewmodel-muzzle align tracer origins with a custom viewmodel's live muzzle without changing authoritative shots
|
|
9
|
+
*/
|
|
10
|
+
export declare function registerFirstPersonMuzzle(camera: Camera, reader: FirstPersonMuzzleReader): () => void;
|
|
11
|
+
/** @internal Stock rig registration shares camera isolation and owner-scoped cleanup. */
|
|
12
|
+
export declare function registerTrackedFirstPersonMuzzle(camera: Camera, reader: FirstPersonMuzzleReader): () => void;
|
|
13
|
+
/** @internal Unscoped legacy reads consider the newest 64 registrations per priority. */
|
|
14
|
+
export declare function readRegisteredFirstPersonMuzzle(target: Vector3, camera?: Camera): boolean;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Camera, Vector3 } from "three";
|
|
2
|
+
const MAX_READERS = 64;
|
|
3
|
+
const custom = new WeakMap();
|
|
4
|
+
const tracked = new WeakMap();
|
|
5
|
+
const legacyCustom = [];
|
|
6
|
+
const legacyTracked = [];
|
|
7
|
+
function register(camera, reader, scoped, legacy) {
|
|
8
|
+
const readers = scoped.get(camera) ?? [];
|
|
9
|
+
if (readers.length >= MAX_READERS)
|
|
10
|
+
throw new RangeError("Too many muzzle readers for one camera");
|
|
11
|
+
const entry = { reader, scratch: new Vector3(), reading: false, disposed: false };
|
|
12
|
+
readers.push(entry);
|
|
13
|
+
scoped.set(camera, readers);
|
|
14
|
+
legacy.push(entry);
|
|
15
|
+
let disposed = false;
|
|
16
|
+
return () => {
|
|
17
|
+
if (disposed)
|
|
18
|
+
return;
|
|
19
|
+
disposed = true;
|
|
20
|
+
entry.disposed = true;
|
|
21
|
+
readers.splice(readers.indexOf(entry), 1);
|
|
22
|
+
legacy.splice(legacy.indexOf(entry), 1);
|
|
23
|
+
if (readers.length === 0)
|
|
24
|
+
scoped.delete(camera);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Register a custom rig's presentation muzzle for its Three.js camera. Newest available custom
|
|
29
|
+
* reader wins over that camera's stock muzzle; call the returned cleanup on unmount.
|
|
30
|
+
*
|
|
31
|
+
* @capability custom-viewmodel-muzzle align tracer origins with a custom viewmodel's live muzzle without changing authoritative shots
|
|
32
|
+
*/
|
|
33
|
+
export function registerFirstPersonMuzzle(camera, reader) {
|
|
34
|
+
return register(camera, reader, custom, legacyCustom);
|
|
35
|
+
}
|
|
36
|
+
/** @internal Stock rig registration shares camera isolation and owner-scoped cleanup. */
|
|
37
|
+
export function registerTrackedFirstPersonMuzzle(camera, reader) {
|
|
38
|
+
return register(camera, reader, tracked, legacyTracked);
|
|
39
|
+
}
|
|
40
|
+
function read(readers, target) {
|
|
41
|
+
if (readers === undefined)
|
|
42
|
+
return false;
|
|
43
|
+
const pending = readers.slice(-MAX_READERS);
|
|
44
|
+
for (let i = pending.length - 1; i >= 0; i--) {
|
|
45
|
+
const entry = pending[i];
|
|
46
|
+
if (entry.reading || entry.disposed)
|
|
47
|
+
continue;
|
|
48
|
+
entry.reading = true;
|
|
49
|
+
try {
|
|
50
|
+
if (!entry.reader(entry.scratch))
|
|
51
|
+
continue;
|
|
52
|
+
const { x, y, z } = entry.scratch;
|
|
53
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z))
|
|
54
|
+
continue;
|
|
55
|
+
target.copy(entry.scratch);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
entry.reading = false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
/** @internal Unscoped legacy reads consider the newest 64 registrations per priority. */
|
|
65
|
+
export function readRegisteredFirstPersonMuzzle(target, camera) {
|
|
66
|
+
return read(camera === undefined ? legacyCustom : custom.get(camera), target) ||
|
|
67
|
+
read(camera === undefined ? legacyTracked : tracked.get(camera), target);
|
|
68
|
+
}
|
package/dist/camera/index.d.ts
CHANGED
|
@@ -9,3 +9,4 @@ export { ControllerCameraShake, type ControllerCameraShakeProps } from "./Contro
|
|
|
9
9
|
export { rtsPanKeysConflict } from "./rigMath.js";
|
|
10
10
|
export { GAME_SIM_FRAME_PRIORITY } from "./orbitCameraMath.js";
|
|
11
11
|
export { CAMERA_TRANSPARENT_USERDATA, isCameraOccluderTransparent, type CameraOccluder, } from "./cameraCollision.js";
|
|
12
|
+
export { registerFirstPersonMuzzle, type FirstPersonMuzzleReader } from "./customMuzzle.js";
|
package/dist/camera/index.js
CHANGED
|
@@ -9,3 +9,4 @@ export { ControllerCameraShake } from "./ControllerCameraShake.js";
|
|
|
9
9
|
export { rtsPanKeysConflict } from "./rigMath.js";
|
|
10
10
|
export { GAME_SIM_FRAME_PRIORITY } from "./orbitCameraMath.js";
|
|
11
11
|
export { CAMERA_TRANSPARENT_USERDATA, isCameraOccluderTransparent, } from "./cameraCollision.js";
|
|
12
|
+
export { registerFirstPersonMuzzle } from "./customMuzzle.js";
|
|
@@ -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,
|
package/dist/camera/rigMath.d.ts
CHANGED
|
@@ -136,6 +136,23 @@ export interface ResolvedChase {
|
|
|
136
136
|
velocityYawResponse: number;
|
|
137
137
|
/** Exponential smoothing rate the anchor yaw follows the body's facing with (#1370). `Infinity` restores the legacy rigid follow. */
|
|
138
138
|
yawResponse: number;
|
|
139
|
+
fovBase: number;
|
|
140
|
+
fovMax: number;
|
|
141
|
+
fovSpeedForMax: number;
|
|
142
|
+
/** Exponential FOV smoothing rate; `Infinity` applies the curve unsmoothed. */
|
|
143
|
+
fovResponse: number;
|
|
144
|
+
distanceExtra: number;
|
|
145
|
+
distanceSpeedForMax: number;
|
|
146
|
+
/** Fraction of body pitch the boom follows; 0 keeps a level boom. */
|
|
147
|
+
pitchBlend: number;
|
|
148
|
+
pitchMax: number;
|
|
149
|
+
pitchResponse: number;
|
|
150
|
+
fovKickDecay: number;
|
|
151
|
+
fovKickMax: number;
|
|
152
|
+
lookBackAction: string | null;
|
|
153
|
+
collision: boolean;
|
|
154
|
+
collisionRadius: number;
|
|
155
|
+
collisionMinDistance: number;
|
|
139
156
|
}
|
|
140
157
|
/** @internal */
|
|
141
158
|
export declare function resolveChase(config: ChaseCameraConfig | undefined): ResolvedChase;
|
|
@@ -143,6 +160,10 @@ export declare function resolveChase(config: ChaseCameraConfig | undefined): Res
|
|
|
143
160
|
* @internal
|
|
144
161
|
*/
|
|
145
162
|
export declare function leadFollowPoint(follow: Vec3, previous: Vec3, dt: number, resolved: ResolvedChase): Vec3;
|
|
163
|
+
/** Velocity-lead the follow point along a known planar velocity, clamped to `leadMax`.
|
|
164
|
+
* @internal
|
|
165
|
+
*/
|
|
166
|
+
export declare function leadFromVelocity(follow: Vec3, velocity: Vec3, resolved: ResolvedChase): Vec3;
|
|
146
167
|
/** One smoothing step of the chase rig's turn-bank roll (#286.10): roll opposes yaw rate, clamped and exponentially damped.
|
|
147
168
|
* @internal
|
|
148
169
|
*/
|
|
@@ -159,14 +180,71 @@ export declare function resolveObserver(config: ObserverCameraConfig | undefined
|
|
|
159
180
|
* @internal
|
|
160
181
|
*/
|
|
161
182
|
export declare function observerPose(subject: Vec3, angle: number, resolved: ResolvedObserver, fov: number): CameraPose;
|
|
162
|
-
/** Desired chase-camera position behind a vehicle facing `yaw` (before spring smoothing).
|
|
183
|
+
/** Desired chase-camera position behind a vehicle facing `yaw` (before spring smoothing). `pitch` (nose-up positive) tilts the boom around the vehicle's right axis.
|
|
184
|
+
* @internal
|
|
185
|
+
*/
|
|
186
|
+
export declare function chaseDesiredPosition(follow: Vec3, yaw: number, resolved: ResolvedChase, pitch?: number, distance?: number): Vec3;
|
|
187
|
+
/** Camera offset from the follow point for a boom `distance` back and `height` up, tilted by `pitch` (nose-up positive).
|
|
188
|
+
* @internal
|
|
189
|
+
*/
|
|
190
|
+
export declare function chaseBoomOffset(yaw: number, pitch: number, distance: number, height: number): Vec3;
|
|
191
|
+
/** Look point ahead of / above a chased vehicle, tilted by `pitch` like the boom.
|
|
163
192
|
* @internal
|
|
164
193
|
*/
|
|
165
|
-
export declare function
|
|
166
|
-
/**
|
|
194
|
+
export declare function chaseLookAt(follow: Vec3, yaw: number, resolved: ResolvedChase, pitch?: number): Vec3;
|
|
195
|
+
/** Mutable per-rig chase state; {@link stepChase} advances it in place. */
|
|
196
|
+
export interface ChaseRigState {
|
|
197
|
+
/** Smoothed camera offset from the led follow point; springing the offset, not the world position, keeps the boom length at any constant speed. */
|
|
198
|
+
offset: Vec3 | null;
|
|
199
|
+
anchorYaw: number | null;
|
|
200
|
+
lastYaw: number | null;
|
|
201
|
+
lastFollow: Vec3 | null;
|
|
202
|
+
roll: number;
|
|
203
|
+
pitch: number;
|
|
204
|
+
fov: number | null;
|
|
205
|
+
kick: number;
|
|
206
|
+
lookingBack: boolean;
|
|
207
|
+
}
|
|
208
|
+
/** @internal */
|
|
209
|
+
export declare function createChaseRigState(): ChaseRigState;
|
|
210
|
+
/** What the chase rig reads from its target each frame. */
|
|
211
|
+
export interface ChaseSample {
|
|
212
|
+
follow: Vec3;
|
|
213
|
+
/** Body heading (radians). */
|
|
214
|
+
yaw: number;
|
|
215
|
+
/** Body `rotationX` (three.js convention: positive = nose down). */
|
|
216
|
+
bodyPitch: number;
|
|
217
|
+
/** Sim velocity when the entity publishes one; `null` falls back to the frame-to-frame position delta. */
|
|
218
|
+
velocity: Vec3 | null;
|
|
219
|
+
lookBack: boolean;
|
|
220
|
+
/** FOV impulse (degrees) queued since the last frame. */
|
|
221
|
+
fovKick: number;
|
|
222
|
+
}
|
|
223
|
+
/** One frame of chase output from {@link stepChase}. */
|
|
224
|
+
export interface ChaseStepResult {
|
|
225
|
+
pose: CameraPose;
|
|
226
|
+
/** Camera roll to apply after the pose, radians. */
|
|
227
|
+
roll: number;
|
|
228
|
+
/** Forward chase yaw to report to the shell; unaffected by look-back. */
|
|
229
|
+
anchorYaw: number;
|
|
230
|
+
/** Planar speed used for FOV, distance and shake. */
|
|
231
|
+
speed: number;
|
|
232
|
+
}
|
|
233
|
+
/** Pulls a desired camera position in toward `pivot` when something blocks the boom. */
|
|
234
|
+
export type ChaseBoomClamp = (pivot: Vec3, desired: Vec3) => Vec3;
|
|
235
|
+
/**
|
|
236
|
+
* One frame of the chase rig: speed from sim velocity, smoothed FOV plus decaying kicks, drift-lag
|
|
237
|
+
* anchor yaw, pitch follow, distance by speed, look-back, and a boom spring that eases the offset
|
|
238
|
+
* from the target rather than its world position, so the camera holds its distance at speed.
|
|
239
|
+
* @internal
|
|
240
|
+
*/
|
|
241
|
+
export declare function stepChase(state: ChaseRigState, sample: ChaseSample, resolved: ResolvedChase, dt: number, clampBoom?: ChaseBoomClamp): ChaseStepResult;
|
|
242
|
+
/**
|
|
243
|
+
* Where a blocked boom should put the camera: `hitDistance` along the pivot→desired ray, less
|
|
244
|
+
* `radius`, but never nearer the pivot than `minDistance`. `null` hit returns `desired` unchanged.
|
|
167
245
|
* @internal
|
|
168
246
|
*/
|
|
169
|
-
export declare function
|
|
247
|
+
export declare function clampBoomToHit(pivot: Vec3, desired: Vec3, hitDistance: number | null, radius: number, minDistance: number): Vec3;
|
|
170
248
|
/**
|
|
171
249
|
* Drift-lag (#1051): the yaw the chase anchor should aim before temporal
|
|
172
250
|
* smoothing. When the target slides, its planar `velocity` diverges from its
|
package/dist/camera/rigMath.js
CHANGED
|
@@ -253,6 +253,9 @@ export function lockOnPose(player, target, config, fov) {
|
|
|
253
253
|
const DEFAULT_CHASE_YAW_RESPONSE = 5;
|
|
254
254
|
/** @internal */
|
|
255
255
|
export function resolveChase(config) {
|
|
256
|
+
const fovSpeedForMax = config?.fov?.speedForMax ?? 24;
|
|
257
|
+
const collision = config?.collision ?? true;
|
|
258
|
+
const collisionBlock = typeof collision === "object" ? collision : undefined;
|
|
256
259
|
return {
|
|
257
260
|
distance: config?.distance ?? 6,
|
|
258
261
|
height: config?.height ?? 2.6,
|
|
@@ -268,16 +271,39 @@ export function resolveChase(config) {
|
|
|
268
271
|
velocityYawMinSpeed: config?.velocityYaw?.minSpeed ?? 4,
|
|
269
272
|
velocityYawResponse: config?.velocityYaw?.response ?? 6,
|
|
270
273
|
yawResponse: config?.yawResponse ?? DEFAULT_CHASE_YAW_RESPONSE,
|
|
274
|
+
fovBase: config?.fov?.base ?? 55,
|
|
275
|
+
fovMax: config?.fov?.max ?? 78,
|
|
276
|
+
fovSpeedForMax,
|
|
277
|
+
fovResponse: config?.fov?.response ?? 6,
|
|
278
|
+
distanceExtra: config?.distanceBySpeed?.extra ?? 0,
|
|
279
|
+
distanceSpeedForMax: config?.distanceBySpeed?.speedForMax ?? fovSpeedForMax,
|
|
280
|
+
pitchBlend: config?.pitchFollow === undefined ? 0 : config.pitchFollow.blend ?? 1,
|
|
281
|
+
pitchMax: config?.pitchFollow?.max ?? 0.6,
|
|
282
|
+
pitchResponse: config?.pitchFollow?.response ?? 5,
|
|
283
|
+
fovKickDecay: config?.fovKick?.decay ?? 7,
|
|
284
|
+
fovKickMax: config?.fovKick?.max ?? 20,
|
|
285
|
+
lookBackAction: config?.lookBackAction ?? null,
|
|
286
|
+
collision: collision !== false,
|
|
287
|
+
collisionRadius: collisionBlock?.radius ?? 0.3,
|
|
288
|
+
collisionMinDistance: collisionBlock?.minDistance ?? 0.8,
|
|
271
289
|
};
|
|
272
290
|
}
|
|
273
291
|
/** Velocity-lead the follow point (#286.9): aim `leadTime` seconds along the target's frame velocity, clamped to `leadMax`.
|
|
274
292
|
* @internal
|
|
275
293
|
*/
|
|
276
294
|
export function leadFollowPoint(follow, previous, dt, resolved) {
|
|
277
|
-
if (
|
|
295
|
+
if (dt <= 0)
|
|
278
296
|
return follow;
|
|
279
|
-
|
|
280
|
-
|
|
297
|
+
return leadFromVelocity(follow, { x: (follow.x - previous.x) / dt, y: 0, z: (follow.z - previous.z) / dt }, resolved);
|
|
298
|
+
}
|
|
299
|
+
/** Velocity-lead the follow point along a known planar velocity, clamped to `leadMax`.
|
|
300
|
+
* @internal
|
|
301
|
+
*/
|
|
302
|
+
export function leadFromVelocity(follow, velocity, resolved) {
|
|
303
|
+
if (resolved.leadTime <= 0)
|
|
304
|
+
return follow;
|
|
305
|
+
let leadX = velocity.x * resolved.leadTime;
|
|
306
|
+
let leadZ = velocity.z * resolved.leadTime;
|
|
281
307
|
const magnitude = Math.hypot(leadX, leadZ);
|
|
282
308
|
if (magnitude > resolved.leadMax) {
|
|
283
309
|
leadX *= resolved.leadMax / magnitude;
|
|
@@ -327,28 +353,120 @@ export function observerPose(subject, angle, resolved, fov) {
|
|
|
327
353
|
fov,
|
|
328
354
|
};
|
|
329
355
|
}
|
|
330
|
-
/** Desired chase-camera position behind a vehicle facing `yaw` (before spring smoothing).
|
|
356
|
+
/** Desired chase-camera position behind a vehicle facing `yaw` (before spring smoothing). `pitch` (nose-up positive) tilts the boom around the vehicle's right axis.
|
|
331
357
|
* @internal
|
|
332
358
|
*/
|
|
333
|
-
export function chaseDesiredPosition(follow, yaw, resolved) {
|
|
334
|
-
const
|
|
359
|
+
export function chaseDesiredPosition(follow, yaw, resolved, pitch = 0, distance = resolved.distance) {
|
|
360
|
+
const offset = chaseBoomOffset(yaw, pitch, distance, resolved.height);
|
|
361
|
+
return { x: follow.x + offset.x, y: follow.y + offset.y, z: follow.z + offset.z };
|
|
362
|
+
}
|
|
363
|
+
/** Camera offset from the follow point for a boom `distance` back and `height` up, tilted by `pitch` (nose-up positive).
|
|
364
|
+
* @internal
|
|
365
|
+
*/
|
|
366
|
+
export function chaseBoomOffset(yaw, pitch, distance, height) {
|
|
367
|
+
const forward = forwardVector(yaw);
|
|
368
|
+
const cos = Math.cos(pitch);
|
|
369
|
+
const sin = Math.sin(pitch);
|
|
370
|
+
const along = -distance * cos - height * sin;
|
|
371
|
+
return { x: forward.x * along, y: -distance * sin + height * cos, z: forward.z * along };
|
|
372
|
+
}
|
|
373
|
+
/** Look point ahead of / above a chased vehicle, tilted by `pitch` like the boom.
|
|
374
|
+
* @internal
|
|
375
|
+
*/
|
|
376
|
+
export function chaseLookAt(follow, yaw, resolved, pitch = 0) {
|
|
377
|
+
const forward = forwardVector(yaw);
|
|
378
|
+
const cos = Math.cos(pitch);
|
|
379
|
+
const sin = Math.sin(pitch);
|
|
380
|
+
const along = 2 * cos - resolved.lookHeight * sin;
|
|
335
381
|
return {
|
|
336
|
-
x: follow.x
|
|
337
|
-
y: follow.y + resolved.
|
|
338
|
-
z: follow.z
|
|
382
|
+
x: follow.x + forward.x * along,
|
|
383
|
+
y: follow.y + 2 * sin + resolved.lookHeight * cos,
|
|
384
|
+
z: follow.z + forward.z * along,
|
|
339
385
|
};
|
|
340
386
|
}
|
|
341
|
-
/**
|
|
387
|
+
/** @internal */
|
|
388
|
+
export function createChaseRigState() {
|
|
389
|
+
return { offset: null, anchorYaw: null, lastYaw: null, lastFollow: null, roll: 0, pitch: 0, fov: null, kick: 0, lookingBack: false };
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* One frame of the chase rig: speed from sim velocity, smoothed FOV plus decaying kicks, drift-lag
|
|
393
|
+
* anchor yaw, pitch follow, distance by speed, look-back, and a boom spring that eases the offset
|
|
394
|
+
* from the target rather than its world position, so the camera holds its distance at speed.
|
|
342
395
|
* @internal
|
|
343
396
|
*/
|
|
344
|
-
export function
|
|
345
|
-
const
|
|
397
|
+
export function stepChase(state, sample, resolved, dt, clampBoom) {
|
|
398
|
+
const { follow, yaw } = sample;
|
|
399
|
+
const last = state.lastFollow ?? follow;
|
|
400
|
+
const velocity = sample.velocity ?? (dt > 0 ? { x: (follow.x - last.x) / dt, y: 0, z: (follow.z - last.z) / dt } : { x: 0, y: 0, z: 0 });
|
|
401
|
+
state.lastFollow = follow;
|
|
402
|
+
const speed = Math.hypot(velocity.x, velocity.z);
|
|
403
|
+
const lastYaw = state.lastYaw ?? yaw;
|
|
404
|
+
state.lastYaw = yaw;
|
|
405
|
+
state.roll = bankRollStep(state.roll, yaw, lastYaw, dt, resolved);
|
|
406
|
+
const targetFov = speedToFov(speed, { base: resolved.fovBase, max: resolved.fovMax, speedForMax: resolved.fovSpeedForMax });
|
|
407
|
+
state.fov =
|
|
408
|
+
state.fov === null || !Number.isFinite(resolved.fovResponse)
|
|
409
|
+
? targetFov
|
|
410
|
+
: state.fov + (targetFov - state.fov) * smoothBlend(dt, resolved.fovResponse);
|
|
411
|
+
state.kick = clamp(state.kick * Math.exp(-resolved.fovKickDecay * dt) + sample.fovKick, -resolved.fovKickMax, resolved.fovKickMax);
|
|
412
|
+
let targetYaw = yaw;
|
|
413
|
+
let response = resolved.yawResponse;
|
|
414
|
+
if (resolved.velocityYawBlend > 0) {
|
|
415
|
+
targetYaw = velocityYawTarget(yaw, velocity, resolved);
|
|
416
|
+
response = resolved.velocityYawResponse;
|
|
417
|
+
}
|
|
418
|
+
const anchorYaw = Number.isFinite(response)
|
|
419
|
+
? smoothYaw(state.anchorYaw ?? targetYaw, targetYaw, response, dt)
|
|
420
|
+
: targetYaw;
|
|
421
|
+
state.anchorYaw = anchorYaw;
|
|
422
|
+
const pitchTarget = clamp(-sample.bodyPitch * resolved.pitchBlend, -resolved.pitchMax, resolved.pitchMax);
|
|
423
|
+
state.pitch =
|
|
424
|
+
resolved.pitchBlend <= 0 ? 0 : state.pitch + (pitchTarget - state.pitch) * smoothBlend(dt, resolved.pitchResponse);
|
|
425
|
+
const snap = state.offset === null || sample.lookBack !== state.lookingBack;
|
|
426
|
+
state.lookingBack = sample.lookBack;
|
|
427
|
+
const boomYaw = sample.lookBack ? anchorYaw + Math.PI : anchorYaw;
|
|
428
|
+
const boomPitch = sample.lookBack ? -state.pitch : state.pitch;
|
|
429
|
+
const extra = resolved.distanceSpeedForMax > 0 ? resolved.distanceExtra * clamp(speed / resolved.distanceSpeedForMax, 0, 1) : 0;
|
|
430
|
+
const led = sample.lookBack ? follow : leadFromVelocity(follow, velocity, resolved);
|
|
431
|
+
const desiredOffset = chaseBoomOffset(boomYaw, boomPitch, resolved.distance + extra, resolved.height);
|
|
432
|
+
let offset = snap || state.offset === null ? desiredOffset : springArmStep(state.offset, desiredOffset, resolved.springDamping, dt);
|
|
433
|
+
if (clampBoom !== undefined) {
|
|
434
|
+
const up = chaseLookAt(led, boomYaw, resolved, boomPitch);
|
|
435
|
+
const pivot = { x: led.x, y: up.y, z: led.z };
|
|
436
|
+
const wanted = { x: led.x + offset.x, y: led.y + offset.y, z: led.z + offset.z };
|
|
437
|
+
const allowed = clampBoom(pivot, wanted);
|
|
438
|
+
offset = { x: allowed.x - led.x, y: allowed.y - led.y, z: allowed.z - led.z };
|
|
439
|
+
}
|
|
440
|
+
state.offset = offset;
|
|
346
441
|
return {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
442
|
+
pose: {
|
|
443
|
+
position: { x: led.x + offset.x, y: led.y + offset.y, z: led.z + offset.z },
|
|
444
|
+
lookAt: chaseLookAt(led, boomYaw, resolved, boomPitch),
|
|
445
|
+
fov: state.fov + state.kick,
|
|
446
|
+
},
|
|
447
|
+
roll: sample.lookBack ? -state.roll : state.roll,
|
|
448
|
+
anchorYaw,
|
|
449
|
+
speed,
|
|
350
450
|
};
|
|
351
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Where a blocked boom should put the camera: `hitDistance` along the pivot→desired ray, less
|
|
454
|
+
* `radius`, but never nearer the pivot than `minDistance`. `null` hit returns `desired` unchanged.
|
|
455
|
+
* @internal
|
|
456
|
+
*/
|
|
457
|
+
export function clampBoomToHit(pivot, desired, hitDistance, radius, minDistance) {
|
|
458
|
+
if (hitDistance === null)
|
|
459
|
+
return desired;
|
|
460
|
+
const dx = desired.x - pivot.x;
|
|
461
|
+
const dy = desired.y - pivot.y;
|
|
462
|
+
const dz = desired.z - pivot.z;
|
|
463
|
+
const length = Math.hypot(dx, dy, dz);
|
|
464
|
+
if (length <= 1e-6)
|
|
465
|
+
return desired;
|
|
466
|
+
const allowed = clamp(hitDistance - radius, Math.min(minDistance, length), length);
|
|
467
|
+
const scale = allowed / length;
|
|
468
|
+
return { x: pivot.x + dx * scale, y: pivot.y + dy * scale, z: pivot.z + dz * scale };
|
|
469
|
+
}
|
|
352
470
|
/**
|
|
353
471
|
* Drift-lag (#1051): the yaw the chase anchor should aim before temporal
|
|
354
472
|
* smoothing. When the target slides, its planar `velocity` diverges from its
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { CameraRigKind, GameCameraConfig } from "@jgengine/core/game/playableGame";
|
|
2
|
+
import type { CameraRigOverride } from "@jgengine/core/runtime/cameraDirector";
|
|
3
|
+
/** @internal */
|
|
4
|
+
export declare function resolveRuntimeCameraConfig(config: GameCameraConfig | undefined, override: CameraRigOverride | null | undefined): GameCameraConfig | undefined;
|
|
2
5
|
/**
|
|
3
6
|
* Resolves which rig mounts from a `GameCameraConfig`. Precedence, most to least
|
|
4
7
|
* specific: an explicit `rig` field always wins; then `perspective: "first"`
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/** @internal */
|
|
2
|
+
export function resolveRuntimeCameraConfig(config, override) {
|
|
3
|
+
if (override === null || override === undefined)
|
|
4
|
+
return config;
|
|
5
|
+
const merged = { ...config, ...override.config, rig: override.kind };
|
|
6
|
+
for (const key of ["firstPerson", "topDown", "rts", "shoulder", "lockOn", "chase", "observer", "turntable", "sideScroll", "inspection", "shake"]) {
|
|
7
|
+
if (override.config[key] !== undefined)
|
|
8
|
+
Object.assign(merged, { [key]: { ...config?.[key], ...override.config[key] } });
|
|
9
|
+
}
|
|
10
|
+
return merged;
|
|
11
|
+
}
|
|
1
12
|
/**
|
|
2
13
|
* Resolves which rig mounts from a `GameCameraConfig`. Precedence, most to least
|
|
3
14
|
* specific: an explicit `rig` field always wins; then `perspective: "first"`
|