@jgengine/shell 0.14.0 → 0.15.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 +500 -1
- package/dist/GamePlayerShell.js +25 -5
- package/dist/Shell3dPresentation.d.ts +6 -6
- package/dist/Shell3dPresentation.js +4 -4
- package/dist/audio/synthEngine.js +12 -2
- package/dist/camera/ControllerCameraShake.d.ts +31 -0
- package/dist/camera/ControllerCameraShake.js +34 -0
- package/dist/camera/GameCameraRig.js +10 -5
- package/dist/camera/GameOrbitCamera.js +1 -10
- package/dist/camera/cameraRigs.js +12 -4
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/rigMath.d.ts +2 -0
- package/dist/camera/rigMath.js +7 -0
- package/dist/devtools/CollisionDebugWorld.js +2 -2
- package/dist/devtools/DevtoolsOverlay.js +2 -0
- package/dist/drivers/FrameDriver.d.ts +4 -1
- package/dist/drivers/FrameDriver.js +5 -3
- package/dist/environment/DayNightSky.d.ts +33 -0
- package/dist/environment/DayNightSky.js +51 -0
- package/dist/environment/Daylight.d.ts +3 -1
- package/dist/environment/Daylight.js +7 -3
- package/dist/environment/EnvironmentScene.js +6 -6
- package/dist/environment/RoadJunctions.d.ts +8 -4
- package/dist/environment/RoadJunctions.js +16 -15
- package/dist/environment/RoadRibbons.d.ts +42 -6
- package/dist/environment/RoadRibbons.js +150 -27
- package/dist/environment/VolumetricClouds.js +3 -3
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/gameKit.d.ts +7 -4
- package/dist/gameKit.js +8 -5
- package/dist/inputSink.js +14 -0
- package/dist/materialOverride.js +30 -0
- package/dist/postfx/PostProcessing.d.ts +3 -2
- package/dist/postfx/PostProcessing.js +11 -3
- package/dist/postfx/ScreenEffectsOverlay.d.ts +32 -0
- package/dist/postfx/ScreenEffectsOverlay.js +70 -0
- package/dist/presentationResolve.d.ts +3 -2
- package/dist/presentationResolve.js +11 -6
- package/dist/render/CascadedShadows.d.ts +13 -0
- package/dist/render/CascadedShadows.js +96 -0
- package/dist/render/EnvironmentLighting.d.ts +23 -7
- package/dist/render/EnvironmentLighting.js +69 -10
- package/dist/render/PartMotion.js +4 -4
- package/dist/render/SceneLighting.js +7 -0
- package/dist/render/SceneModels.d.ts +37 -1
- package/dist/render/SceneModels.js +40 -11
- package/dist/render/modelLoad.d.ts +41 -5
- package/dist/render/modelLoad.js +85 -18
- package/dist/render/sceneCapture.d.ts +27 -0
- package/dist/render/sceneCapture.js +43 -0
- package/dist/render/useDisposable.d.ts +16 -0
- package/dist/render/useDisposable.js +24 -0
- package/dist/render/useModelAnimation.js +47 -4
- package/dist/scene/AuthoredScene.js +14 -24
- package/dist/scene/CityRenderer.js +103 -36
- package/dist/scene/GrassFieldRenderer.js +4 -1
- package/dist/scene/PoleLineRenderer.js +7 -16
- package/dist/settings/QuickControls.js +5 -3
- package/dist/settings/SettingsMenu.js +6 -3
- package/dist/settings/appliedSettings.d.ts +3 -1
- package/dist/settings/appliedSettings.js +17 -5
- package/dist/settings/settingsController.js +11 -5
- package/dist/shellMovement.d.ts +15 -1
- package/dist/shellMovement.js +15 -1
- package/dist/structures/GeneratedBuilding.js +4 -5
- package/dist/structures/TransformGizmo.js +6 -5
- package/dist/terrain/CarvedTerrain.js +3 -7
- package/dist/terrain/GrassField.js +2 -2
- package/dist/terrain/ProceduralGround.js +3 -5
- package/dist/terrain/grassMaterial.d.ts +8 -2
- package/dist/terrain/grassMaterial.js +24 -5
- package/dist/terrain/index.d.ts +1 -1
- package/dist/terrain/terrainDetailMaterial.js +11 -2
- package/dist/terrain/terrainMath.d.ts +38 -1
- package/dist/terrain/terrainMath.js +86 -64
- package/dist/touch/TouchControlsOverlay.d.ts +6 -2
- package/dist/touch/TouchControlsOverlay.js +136 -17
- package/dist/vfx/ParticleField.d.ts +32 -0
- package/dist/vfx/ParticleField.js +91 -0
- package/dist/vfx/WorldFloatingText.d.ts +32 -0
- package/dist/vfx/WorldFloatingText.js +32 -0
- package/dist/water/Ocean.js +3 -6
- package/dist/weather/LightningStrike.js +3 -9
- package/dist/weather/RainField.js +3 -7
- package/dist/weather/SnowField.js +3 -7
- package/dist/world/DataObjects.js +3 -6
- package/dist/world/GridWorldScene.js +3 -6
- package/dist/world/InstancedBodies.js +3 -6
- package/dist/world/InstancedJoints.js +2 -2
- package/dist/world/SpriteBatch.js +3 -6
- package/dist/world/WorldEntityFrames.d.ts +47 -0
- package/dist/world/WorldEntityFrames.js +75 -0
- package/dist/world/WorldHud.js +7 -5
- package/dist/world/WorldPings.d.ts +26 -0
- package/dist/world/WorldPings.js +68 -0
- package/dist/world/WorldScene.js +9 -4
- package/dist/world/pingPulse.d.ts +21 -0
- package/dist/world/pingPulse.js +17 -0
- package/package.json +5 -5
- package/skills/ce-handoff/SKILL.md +149 -0
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +3 -1
- package/skills/jgengine/api.md +17 -7
- package/skills/jgengine/capabilities.md +12 -0
- package/skills/jgengine/recipes/minimal-game.md +7 -4
- package/skills/jgengine-ui/SKILL.md +5 -3
- package/skills/jgengine-ui/api.md +708 -19
- package/skills/jgengine-ui/capabilities.md +362 -1
- package/skills/jgengine-ui/recipes/overhead-entity-frames.md +136 -0
- package/skills/jgengine-ui/reference.md +7 -4
- package/skills/jgengine-world/api.md +397 -11
- package/skills/jgengine-world/capabilities.md +123 -2
- package/skills/jgengine-world/reference.md +15 -1
|
@@ -5,6 +5,7 @@ import { EffectComposer, RenderPass, UnrealBloomPass } from "three-stdlib";
|
|
|
5
5
|
import { OutputPass } from "three/examples/jsm/postprocessing/OutputPass.js";
|
|
6
6
|
import { GTAOPass } from "three/examples/jsm/postprocessing/GTAOPass.js";
|
|
7
7
|
import { BokehPass } from "three/examples/jsm/postprocessing/BokehPass.js";
|
|
8
|
+
import { SMAAPass } from "three/examples/jsm/postprocessing/SMAAPass.js";
|
|
8
9
|
import { createGradePass } from "./gradeShader.js";
|
|
9
10
|
import { hidePostfxOverlays, restorePostfxOverlays } from "./postfxOverlay.js";
|
|
10
11
|
/**
|
|
@@ -59,14 +60,15 @@ function syncSize(built, width, height, pixelRatio) {
|
|
|
59
60
|
/**
|
|
60
61
|
* Mounts an `EffectComposer` inside the shell Canvas and takes over rendering
|
|
61
62
|
* (priority-1 `useFrame`, which disables R3F auto-render) to run the configured
|
|
62
|
-
* post chain: RenderPass → GTAO → UnrealBloom → OutputPass → Grade. Rendered only
|
|
63
|
+
* post chain: RenderPass → GTAO → UnrealBloom → SMAA → OutputPass → Grade. Rendered only
|
|
63
64
|
* when `PlayableGame.postProcessing` is set, so games without it draw unchanged.
|
|
64
65
|
*
|
|
65
66
|
* `quality` (the player's graphics-quality setting) gates the passes whose cost
|
|
66
67
|
* scales with scene geometry or resolution beyond the dpr cap: GTAO re-renders
|
|
67
68
|
* the whole scene for depth/normals and runs a multi-sample full-screen pass,
|
|
68
69
|
* and Bokeh DOF renders scene depth again — both run on "high" only. Bloom,
|
|
69
|
-
* tone mapping, and grade stay on every tier
|
|
70
|
+
* SMAA, tone mapping, and grade stay on every tier (SMAA is the cheap edge fix
|
|
71
|
+
* for alpha-tested foliage that MSAA samples alone leave crawling).
|
|
70
72
|
*/
|
|
71
73
|
export function PostProcessing({ config, quality = "high" }) {
|
|
72
74
|
const gl = useThree((s) => s.gl);
|
|
@@ -77,9 +79,11 @@ export function PostProcessing({ config, quality = "high" }) {
|
|
|
77
79
|
useEffect(() => {
|
|
78
80
|
const width = Math.max(1, size.width);
|
|
79
81
|
const height = Math.max(1, size.height);
|
|
82
|
+
const aa = config.aa === undefined ? "smaa" : config.aa;
|
|
83
|
+
const msaaSamples = aa === false ? 0 : 2;
|
|
80
84
|
const target = new THREE.WebGLRenderTarget(width, height, {
|
|
81
85
|
type: THREE.HalfFloatType,
|
|
82
|
-
samples:
|
|
86
|
+
samples: msaaSamples,
|
|
83
87
|
});
|
|
84
88
|
const composer = new EffectComposer(gl, target);
|
|
85
89
|
composer.addPass(new RenderPass(scene, camera));
|
|
@@ -110,6 +114,10 @@ export function PostProcessing({ config, quality = "high" }) {
|
|
|
110
114
|
});
|
|
111
115
|
composer.addPass(dof);
|
|
112
116
|
}
|
|
117
|
+
// SMAA must run before OutputPass (linear-sRGB). Default on for post chains.
|
|
118
|
+
if (aa === "smaa") {
|
|
119
|
+
composer.addPass(new SMAAPass());
|
|
120
|
+
}
|
|
113
121
|
composer.addPass(new OutputPass());
|
|
114
122
|
let grade = null;
|
|
115
123
|
if (config.grade !== false) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import type { ScreenEffectsController } from "@jgengine/core/vfx/screenEffects";
|
|
3
|
+
/** Reskin tokens for {@link ScreenEffectsOverlay}. */
|
|
4
|
+
export interface ScreenEffectsOverlayProps {
|
|
5
|
+
/** The model to render. */
|
|
6
|
+
controller: ScreenEffectsController;
|
|
7
|
+
/**
|
|
8
|
+
* CSS blend mode for the tint layers. Default `"normal"` (straight alpha
|
|
9
|
+
* composite). `"screen"` or `"multiply"` read as more filmic color grades.
|
|
10
|
+
*/
|
|
11
|
+
blendMode?: CSSProperties["mixBlendMode"];
|
|
12
|
+
/** Extra `z-index` for the overlay root. Default `40`. */
|
|
13
|
+
zIndex?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Drive `advance()` on an animation frame. Default `true`. Turn off if the game
|
|
16
|
+
* already advances the controller from its own tick.
|
|
17
|
+
*/
|
|
18
|
+
animate?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A DOM overlay that renders a {@link ScreenEffectsController} as full-screen
|
|
22
|
+
* color-grade layers over the game canvas — damage vignettes, heal flashes, a
|
|
23
|
+
* sustained low-health pulse — through the shell's postfx overlay layer, so a game
|
|
24
|
+
* gets screen feedback without hand-rolling shader or overlay timing. It subscribes
|
|
25
|
+
* to the model, drives `advance()` on an animation frame (unless the game ticks it),
|
|
26
|
+
* and paints one absolutely-positioned, pointer-transparent layer per active effect
|
|
27
|
+
* at its eased opacity. Presentation only: all timing and state live in the core
|
|
28
|
+
* model, and `kind` is never interpreted here.
|
|
29
|
+
*
|
|
30
|
+
* @capability screen-effects-overlay shell DOM overlay that renders a core screen-effects controller as full-screen flash / edge-vignette / low-health-pulse color-grade layers
|
|
31
|
+
*/
|
|
32
|
+
export declare function ScreenEffectsOverlay({ controller, blendMode, zIndex, animate, }: ScreenEffectsOverlayProps): ReactNode;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useReducer, useRef } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Build the CSS `background` for one effect at its current eased opacity. A
|
|
5
|
+
* `"full"` effect tints the whole frame; a `"vignette"` effect tints only the
|
|
6
|
+
* edges via a radial gradient that stays transparent in the center. Genre-blind —
|
|
7
|
+
* it reads `color`/`shape`/`intensity` off the model and nothing else.
|
|
8
|
+
*/
|
|
9
|
+
function effectBackground(effect) {
|
|
10
|
+
if (effect.shape === "vignette") {
|
|
11
|
+
// Transparent core → the effect color at the corners, scaled by opacity.
|
|
12
|
+
return `radial-gradient(ellipse at center, transparent 40%, ${effect.color} 140%)`;
|
|
13
|
+
}
|
|
14
|
+
return effect.color;
|
|
15
|
+
}
|
|
16
|
+
/** Per-effect fill layer opacity, so `color` can stay a plain CSS color string. */
|
|
17
|
+
function effectOpacity(effect) {
|
|
18
|
+
const a = effect.intensity;
|
|
19
|
+
return a < 0 ? 0 : a > 1 ? 1 : a;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A DOM overlay that renders a {@link ScreenEffectsController} as full-screen
|
|
23
|
+
* color-grade layers over the game canvas — damage vignettes, heal flashes, a
|
|
24
|
+
* sustained low-health pulse — through the shell's postfx overlay layer, so a game
|
|
25
|
+
* gets screen feedback without hand-rolling shader or overlay timing. It subscribes
|
|
26
|
+
* to the model, drives `advance()` on an animation frame (unless the game ticks it),
|
|
27
|
+
* and paints one absolutely-positioned, pointer-transparent layer per active effect
|
|
28
|
+
* at its eased opacity. Presentation only: all timing and state live in the core
|
|
29
|
+
* model, and `kind` is never interpreted here.
|
|
30
|
+
*
|
|
31
|
+
* @capability screen-effects-overlay shell DOM overlay that renders a core screen-effects controller as full-screen flash / edge-vignette / low-health-pulse color-grade layers
|
|
32
|
+
*/
|
|
33
|
+
export function ScreenEffectsOverlay({ controller, blendMode = "normal", zIndex = 40, animate = true, }) {
|
|
34
|
+
const [, bump] = useReducer((n) => n + 1, 0);
|
|
35
|
+
const rafRef = useRef(0);
|
|
36
|
+
useEffect(() => controller.subscribe(bump), [controller]);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!animate)
|
|
39
|
+
return;
|
|
40
|
+
let running = true;
|
|
41
|
+
const loop = () => {
|
|
42
|
+
if (!running)
|
|
43
|
+
return;
|
|
44
|
+
controller.advance();
|
|
45
|
+
rafRef.current = requestAnimationFrame(loop);
|
|
46
|
+
};
|
|
47
|
+
rafRef.current = requestAnimationFrame(loop);
|
|
48
|
+
return () => {
|
|
49
|
+
running = false;
|
|
50
|
+
cancelAnimationFrame(rafRef.current);
|
|
51
|
+
};
|
|
52
|
+
}, [controller, animate]);
|
|
53
|
+
const composite = controller.composite();
|
|
54
|
+
if (composite.length === 0)
|
|
55
|
+
return null;
|
|
56
|
+
const root = {
|
|
57
|
+
position: "absolute",
|
|
58
|
+
inset: 0,
|
|
59
|
+
pointerEvents: "none",
|
|
60
|
+
overflow: "hidden",
|
|
61
|
+
zIndex,
|
|
62
|
+
};
|
|
63
|
+
return (_jsx("div", { style: root, "aria-hidden": "true", "data-jg-screen-effects": "", children: composite.map((effect) => (_jsx("div", { style: {
|
|
64
|
+
position: "absolute",
|
|
65
|
+
inset: 0,
|
|
66
|
+
background: effectBackground(effect),
|
|
67
|
+
opacity: effectOpacity(effect),
|
|
68
|
+
mixBlendMode: blendMode,
|
|
69
|
+
} }, effect.id))) }));
|
|
70
|
+
}
|
|
@@ -31,9 +31,10 @@ export interface ResolvedPresentationEffects {
|
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
33
|
* Resolve `presentationEffects` for the 3D shell.
|
|
34
|
-
* - `undefined` / `true` →
|
|
34
|
+
* - `undefined` / `true` → default channels (telegraphs, vfx, floatText, shake on; tracers off).
|
|
35
35
|
* - `false` → none.
|
|
36
|
-
* - object → per-channel; missing keys default
|
|
36
|
+
* - object → per-channel; missing keys default on, EXCEPT `tracers`, which is opt-in and only
|
|
37
|
+
* enabled by an explicit `tracers: true`.
|
|
37
38
|
* @internal
|
|
38
39
|
*/
|
|
39
40
|
export declare function resolvePresentationEffects(config: boolean | PresentationEffectsConfig | undefined): ResolvedPresentationEffects;
|
|
@@ -17,11 +17,15 @@ export function resolveWorldOverlayBars(config) {
|
|
|
17
17
|
...(config.showHealth === undefined ? {} : { showHealth: config.showHealth }),
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
// Every channel defaults on EXCEPT tracers. `fireProjectile` is a generic seam (bullets,
|
|
21
|
+
// bolts, grenades, launchers), so a muzzle→impact tracer line is only ever right for a subset
|
|
22
|
+
// of shots — it must never appear in a game that never asked for it. Tracers are therefore
|
|
23
|
+
// opt-in: a game turns them on with `presentationEffects: { tracers: true }`.
|
|
24
|
+
const DEFAULTS = {
|
|
21
25
|
telegraphs: true,
|
|
22
26
|
vfx: true,
|
|
23
27
|
floatText: true,
|
|
24
|
-
tracers:
|
|
28
|
+
tracers: false,
|
|
25
29
|
shake: true,
|
|
26
30
|
};
|
|
27
31
|
const ALL_OFF = {
|
|
@@ -33,21 +37,22 @@ const ALL_OFF = {
|
|
|
33
37
|
};
|
|
34
38
|
/**
|
|
35
39
|
* Resolve `presentationEffects` for the 3D shell.
|
|
36
|
-
* - `undefined` / `true` →
|
|
40
|
+
* - `undefined` / `true` → default channels (telegraphs, vfx, floatText, shake on; tracers off).
|
|
37
41
|
* - `false` → none.
|
|
38
|
-
* - object → per-channel; missing keys default
|
|
42
|
+
* - object → per-channel; missing keys default on, EXCEPT `tracers`, which is opt-in and only
|
|
43
|
+
* enabled by an explicit `tracers: true`.
|
|
39
44
|
* @internal
|
|
40
45
|
*/
|
|
41
46
|
export function resolvePresentationEffects(config) {
|
|
42
47
|
if (config === false)
|
|
43
48
|
return ALL_OFF;
|
|
44
49
|
if (config === undefined || config === true)
|
|
45
|
-
return
|
|
50
|
+
return { ...DEFAULTS };
|
|
46
51
|
return {
|
|
47
52
|
telegraphs: config.telegraphs !== false,
|
|
48
53
|
vfx: config.vfx !== false,
|
|
49
54
|
floatText: config.floatText !== false,
|
|
50
|
-
tracers: config.tracers
|
|
55
|
+
tracers: config.tracers === true,
|
|
51
56
|
shake: config.shake !== false,
|
|
52
57
|
};
|
|
53
58
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DirectionalLightingConfig } from "@jgengine/core/game/playableGame";
|
|
2
|
+
/**
|
|
3
|
+
* Cascaded shadow maps for one directional light — outdoor scenes keep shadows past a
|
|
4
|
+
* single 40-unit ortho frustum. Uses three.js `CSM` (shader injection on standard materials).
|
|
5
|
+
*
|
|
6
|
+
* Mount when `entry.cascades > 1` and `castShadow`. Replaces the single R3F directional
|
|
7
|
+
* shadow light for that entry.
|
|
8
|
+
*
|
|
9
|
+
* @internal shell SceneLighting helper — games set `cascades` on directional lighting config.
|
|
10
|
+
*/
|
|
11
|
+
export declare function CascadedShadows({ entry }: {
|
|
12
|
+
entry: DirectionalLightingConfig;
|
|
13
|
+
}): null;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import * as THREE from "three";
|
|
4
|
+
import { CSM } from "three/examples/jsm/csm/CSM.js";
|
|
5
|
+
function isStandardMaterial(mat) {
|
|
6
|
+
return mat instanceof THREE.MeshStandardMaterial || mat instanceof THREE.MeshPhysicalMaterial;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Cascaded shadow maps for one directional light — outdoor scenes keep shadows past a
|
|
10
|
+
* single 40-unit ortho frustum. Uses three.js `CSM` (shader injection on standard materials).
|
|
11
|
+
*
|
|
12
|
+
* Mount when `entry.cascades > 1` and `castShadow`. Replaces the single R3F directional
|
|
13
|
+
* shadow light for that entry.
|
|
14
|
+
*
|
|
15
|
+
* @internal shell SceneLighting helper — games set `cascades` on directional lighting config.
|
|
16
|
+
*/
|
|
17
|
+
export function CascadedShadows({ entry }) {
|
|
18
|
+
const camera = useThree((s) => s.camera);
|
|
19
|
+
const scene = useThree((s) => s.scene);
|
|
20
|
+
const csmRef = useRef(null);
|
|
21
|
+
const patchedRef = useRef(new WeakSet());
|
|
22
|
+
const frameRef = useRef(0);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const cascades = Math.max(2, Math.min(4, Math.floor(entry.cascades ?? 3)));
|
|
25
|
+
const lightDirection = new THREE.Vector3(-entry.position[0], -entry.position[1], -entry.position[2]);
|
|
26
|
+
if (lightDirection.lengthSq() < 1e-8)
|
|
27
|
+
lightDirection.set(-1, -1, -1);
|
|
28
|
+
lightDirection.normalize();
|
|
29
|
+
const csm = new CSM({
|
|
30
|
+
camera,
|
|
31
|
+
parent: scene,
|
|
32
|
+
cascades,
|
|
33
|
+
maxFar: entry.shadowMaxFar ?? 200,
|
|
34
|
+
mode: "practical",
|
|
35
|
+
shadowMapSize: entry.shadowMapSize ?? 1024,
|
|
36
|
+
shadowBias: entry.shadowBias ?? -0.0004,
|
|
37
|
+
lightDirection,
|
|
38
|
+
lightIntensity: entry.intensity ?? 1.3,
|
|
39
|
+
lightNear: 0.5,
|
|
40
|
+
lightFar: Math.max(200, (entry.shadowCameraSize ?? 40) * 6),
|
|
41
|
+
lightMargin: entry.shadowCameraSize ?? 40,
|
|
42
|
+
});
|
|
43
|
+
for (const light of csm.lights) {
|
|
44
|
+
light.color = new THREE.Color(entry.color ?? "#ffffff");
|
|
45
|
+
light.castShadow = true;
|
|
46
|
+
}
|
|
47
|
+
patchedRef.current = new WeakSet();
|
|
48
|
+
patchSceneMaterials(scene, csm, patchedRef.current);
|
|
49
|
+
csmRef.current = csm;
|
|
50
|
+
frameRef.current = 0;
|
|
51
|
+
return () => {
|
|
52
|
+
csm.dispose();
|
|
53
|
+
csmRef.current = null;
|
|
54
|
+
};
|
|
55
|
+
}, [
|
|
56
|
+
camera,
|
|
57
|
+
scene,
|
|
58
|
+
entry.cascades,
|
|
59
|
+
entry.color,
|
|
60
|
+
entry.intensity,
|
|
61
|
+
entry.position[0],
|
|
62
|
+
entry.position[1],
|
|
63
|
+
entry.position[2],
|
|
64
|
+
entry.shadowBias,
|
|
65
|
+
entry.shadowCameraSize,
|
|
66
|
+
entry.shadowMapSize,
|
|
67
|
+
entry.shadowMaxFar,
|
|
68
|
+
]);
|
|
69
|
+
useFrame(() => {
|
|
70
|
+
const csm = csmRef.current;
|
|
71
|
+
if (csm === null)
|
|
72
|
+
return;
|
|
73
|
+
csm.camera = camera;
|
|
74
|
+
frameRef.current += 1;
|
|
75
|
+
// Async model mounts need setupMaterial — re-scan every 30 frames after the first.
|
|
76
|
+
if (frameRef.current === 1 || frameRef.current % 30 === 0) {
|
|
77
|
+
patchSceneMaterials(scene, csm, patchedRef.current);
|
|
78
|
+
}
|
|
79
|
+
csm.update();
|
|
80
|
+
});
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
function patchSceneMaterials(scene, csm, patched) {
|
|
84
|
+
scene.traverse((obj) => {
|
|
85
|
+
const mesh = obj;
|
|
86
|
+
if (!mesh.isMesh)
|
|
87
|
+
return;
|
|
88
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
89
|
+
for (const mat of materials) {
|
|
90
|
+
if (!isStandardMaterial(mat) || patched.has(mat))
|
|
91
|
+
continue;
|
|
92
|
+
csm.setupMaterial(mat);
|
|
93
|
+
patched.add(mat);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -1,10 +1,26 @@
|
|
|
1
|
+
/** @internal Props for shell {@link EnvironmentLighting}. */
|
|
2
|
+
export interface EnvironmentLightingProps {
|
|
3
|
+
/** IBL intensity. Default 0.35. */
|
|
4
|
+
intensity?: number;
|
|
5
|
+
/** Upper-hemisphere tint for the procedural sky probe. Default cool daylight. */
|
|
6
|
+
skyColor?: string;
|
|
7
|
+
/** Ground / lower-hemisphere tint. Default soft earth. */
|
|
8
|
+
groundColor?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Optional sun direction (world space, not normalized). When set, a bright disc
|
|
11
|
+
* is baked into the probe so metals pick up a real sun glint that tracks daylight.
|
|
12
|
+
*/
|
|
13
|
+
sunDirection?: readonly [number, number, number];
|
|
14
|
+
/** Sun disc tint. Default warm white. */
|
|
15
|
+
sunColor?: string;
|
|
16
|
+
}
|
|
1
17
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
18
|
+
* Image-based lighting from a procedural sky/ground probe (not three's stock
|
|
19
|
+
* RoomEnvironment gray studio box). Metals and dielectrics pick up sky color and
|
|
20
|
+
* an optional sun glint; re-render when sky/sun props change so a daylight cycle
|
|
21
|
+
* regenerates the environment map with the authored world.
|
|
22
|
+
*
|
|
23
|
+
* Opt out of cinematic IBL with `look: "flat"` on the shell presentation.
|
|
6
24
|
* @internal shell-internal default lighting; games never import it.
|
|
7
25
|
*/
|
|
8
|
-
export declare function EnvironmentLighting({ intensity }:
|
|
9
|
-
intensity?: number;
|
|
10
|
-
}): null;
|
|
26
|
+
export declare function EnvironmentLighting({ intensity, skyColor, groundColor, sunDirection, sunColor, }: EnvironmentLightingProps): null;
|
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
import { useThree } from "@react-three/fiber";
|
|
2
2
|
import { useEffect } from "react";
|
|
3
3
|
import * as THREE from "three";
|
|
4
|
-
import { RoomEnvironment } from "three/examples/jsm/environments/RoomEnvironment.js";
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Image-based lighting from a procedural sky/ground probe (not three's stock
|
|
6
|
+
* RoomEnvironment gray studio box). Metals and dielectrics pick up sky color and
|
|
7
|
+
* an optional sun glint; re-render when sky/sun props change so a daylight cycle
|
|
8
|
+
* regenerates the environment map with the authored world.
|
|
9
|
+
*
|
|
10
|
+
* Opt out of cinematic IBL with `look: "flat"` on the shell presentation.
|
|
10
11
|
* @internal shell-internal default lighting; games never import it.
|
|
11
12
|
*/
|
|
12
|
-
export function EnvironmentLighting({ intensity = 0.
|
|
13
|
+
export function EnvironmentLighting({ intensity = 0.35, skyColor = "#87b5e0", groundColor = "#3d4a38", sunDirection, sunColor = "#fff2d6", }) {
|
|
13
14
|
const gl = useThree((state) => state.gl);
|
|
14
15
|
const scene = useThree((state) => state.scene);
|
|
15
16
|
useEffect(() => {
|
|
16
17
|
const pmrem = new THREE.PMREMGenerator(gl);
|
|
17
|
-
const
|
|
18
|
-
const target = pmrem.fromScene(
|
|
18
|
+
const probe = buildSkyProbeScene({ skyColor, groundColor, sunDirection, sunColor });
|
|
19
|
+
const target = pmrem.fromScene(probe, 0.04);
|
|
19
20
|
const previous = scene.environment;
|
|
20
21
|
scene.environment = target.texture;
|
|
21
22
|
const sceneWithIntensity = scene;
|
|
22
23
|
const previousIntensity = sceneWithIntensity.environmentIntensity;
|
|
23
24
|
sceneWithIntensity.environmentIntensity = intensity;
|
|
24
|
-
|
|
25
|
+
disposeScene(probe);
|
|
25
26
|
pmrem.dispose();
|
|
26
27
|
return () => {
|
|
27
28
|
target.dispose();
|
|
@@ -29,6 +30,64 @@ export function EnvironmentLighting({ intensity = 0.28 }) {
|
|
|
29
30
|
if (previousIntensity !== undefined)
|
|
30
31
|
sceneWithIntensity.environmentIntensity = previousIntensity;
|
|
31
32
|
};
|
|
32
|
-
}, [gl, scene, intensity]);
|
|
33
|
+
}, [gl, scene, intensity, skyColor, groundColor, sunColor, sunDirection?.[0], sunDirection?.[1], sunDirection?.[2]]);
|
|
33
34
|
return null;
|
|
34
35
|
}
|
|
36
|
+
function buildSkyProbeScene(options) {
|
|
37
|
+
const probe = new THREE.Scene();
|
|
38
|
+
// Inward-facing sphere: sky above, ground below, so PMREM samples a sky dome not a studio box.
|
|
39
|
+
const geo = new THREE.SphereGeometry(50, 32, 16);
|
|
40
|
+
const mat = new THREE.ShaderMaterial({
|
|
41
|
+
side: THREE.BackSide,
|
|
42
|
+
depthWrite: false,
|
|
43
|
+
uniforms: {
|
|
44
|
+
sky: { value: new THREE.Color(options.skyColor) },
|
|
45
|
+
ground: { value: new THREE.Color(options.groundColor) },
|
|
46
|
+
},
|
|
47
|
+
vertexShader: /* glsl */ `
|
|
48
|
+
varying vec3 vDir;
|
|
49
|
+
void main() {
|
|
50
|
+
vDir = normalize(position);
|
|
51
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
52
|
+
}
|
|
53
|
+
`,
|
|
54
|
+
fragmentShader: /* glsl */ `
|
|
55
|
+
uniform vec3 sky;
|
|
56
|
+
uniform vec3 ground;
|
|
57
|
+
varying vec3 vDir;
|
|
58
|
+
void main() {
|
|
59
|
+
float t = clamp(vDir.y * 0.5 + 0.5, 0.0, 1.0);
|
|
60
|
+
// Soft horizon blend so the equator is not a hard band.
|
|
61
|
+
float w = smoothstep(0.0, 1.0, t);
|
|
62
|
+
gl_FragColor = vec4(mix(ground, sky, w), 1.0);
|
|
63
|
+
}
|
|
64
|
+
`,
|
|
65
|
+
});
|
|
66
|
+
probe.add(new THREE.Mesh(geo, mat));
|
|
67
|
+
if (options.sunDirection !== undefined) {
|
|
68
|
+
const dir = new THREE.Vector3(options.sunDirection[0], options.sunDirection[1], options.sunDirection[2]);
|
|
69
|
+
if (dir.lengthSq() > 1e-8) {
|
|
70
|
+
dir.normalize();
|
|
71
|
+
const sunGeo = new THREE.SphereGeometry(3.5, 16, 12);
|
|
72
|
+
const sunMat = new THREE.MeshBasicMaterial({ color: options.sunColor });
|
|
73
|
+
const sun = new THREE.Mesh(sunGeo, sunMat);
|
|
74
|
+
// Place the disc outside the sky sphere sample so it stays a compact highlight.
|
|
75
|
+
sun.position.copy(dir.multiplyScalar(42));
|
|
76
|
+
probe.add(sun);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return probe;
|
|
80
|
+
}
|
|
81
|
+
function disposeScene(scene) {
|
|
82
|
+
scene.traverse((obj) => {
|
|
83
|
+
const mesh = obj;
|
|
84
|
+
if (mesh.isMesh) {
|
|
85
|
+
mesh.geometry?.dispose();
|
|
86
|
+
const material = mesh.material;
|
|
87
|
+
if (Array.isArray(material))
|
|
88
|
+
material.forEach((m) => m.dispose());
|
|
89
|
+
else
|
|
90
|
+
material?.dispose();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -3,7 +3,7 @@ import { useFrame } from "@react-three/fiber";
|
|
|
3
3
|
import { useEffect, useMemo, useRef } from "react";
|
|
4
4
|
import * as THREE from "three";
|
|
5
5
|
import { createPartPose, partMotionPhase, sampleBodyPose, samplePartPose, } from "@jgengine/core/game/partAnimation";
|
|
6
|
-
import {
|
|
6
|
+
import { useOptionalGameContext } from "@jgengine/react/provider";
|
|
7
7
|
const FLINCH_RELEASE_SEC = 0.35;
|
|
8
8
|
const DEATH_RAMP_PER_SEC = 2.5;
|
|
9
9
|
const SPEED_SMOOTHING = 12;
|
|
@@ -16,7 +16,7 @@ const SPEED_SMOOTHING = 12;
|
|
|
16
16
|
* as static kit pieces. Children are the base model content (primitive, attachments).
|
|
17
17
|
*/
|
|
18
18
|
export function PartMotionRig({ parts, model, instanceId, renderPart, children, }) {
|
|
19
|
-
const ctx =
|
|
19
|
+
const ctx = useOptionalGameContext();
|
|
20
20
|
const rootRef = useRef(null);
|
|
21
21
|
const partRefs = useRef([]);
|
|
22
22
|
const motionRef = useRef({
|
|
@@ -27,7 +27,7 @@ export function PartMotionRig({ parts, model, instanceId, renderPart, children,
|
|
|
27
27
|
});
|
|
28
28
|
const phase = useMemo(() => partMotionPhase(instanceId ?? model.url), [instanceId, model.url]);
|
|
29
29
|
useEffect(() => {
|
|
30
|
-
if (instanceId === undefined)
|
|
30
|
+
if (ctx === null || instanceId === undefined)
|
|
31
31
|
return;
|
|
32
32
|
const offHit = ctx.game.events.on("combat.hitReaction", (event) => {
|
|
33
33
|
if (event.instanceId === instanceId)
|
|
@@ -46,7 +46,7 @@ export function PartMotionRig({ parts, model, instanceId, renderPart, children,
|
|
|
46
46
|
const pose = useMemo(() => createPartPose(), []);
|
|
47
47
|
useFrame((frameState, delta) => {
|
|
48
48
|
const motion = motionRef.current;
|
|
49
|
-
if (delta > 0 && instanceId !== undefined) {
|
|
49
|
+
if (delta > 0 && instanceId !== undefined && ctx !== null) {
|
|
50
50
|
const entity = ctx.scene.entity.get(instanceId);
|
|
51
51
|
if (entity !== null) {
|
|
52
52
|
const [x, , z] = entity.position;
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CascadedShadows } from "./CascadedShadows.js";
|
|
2
3
|
const DEFAULT_BACKDROP_FOG_COLOR = "#1a1c22";
|
|
4
|
+
function usesCascades(entry) {
|
|
5
|
+
return (entry.castShadow ?? false) && (entry.cascades ?? 1) > 1;
|
|
6
|
+
}
|
|
3
7
|
function DirectionalShadowLight({ entry }) {
|
|
8
|
+
if (usesCascades(entry)) {
|
|
9
|
+
return _jsx(CascadedShadows, { entry: entry });
|
|
10
|
+
}
|
|
4
11
|
const size = entry.shadowCameraSize ?? 40;
|
|
5
12
|
return (_jsx("directionalLight", { position: [entry.position[0], entry.position[1], entry.position[2]], intensity: entry.intensity ?? 1.3, color: entry.color, castShadow: entry.castShadow ?? false, "shadow-mapSize-width": entry.shadowMapSize ?? 1024, "shadow-mapSize-height": entry.shadowMapSize ?? 1024, "shadow-camera-left": -size, "shadow-camera-right": size, "shadow-camera-top": size, "shadow-camera-bottom": -size, "shadow-camera-near": 0.5, "shadow-camera-far": Math.max(200, size * 6), "shadow-bias": entry.shadowBias ?? -0.0004, "shadow-normalBias": entry.shadowNormalBias ?? 0.02 }));
|
|
6
13
|
}
|
|
@@ -1,8 +1,44 @@
|
|
|
1
|
-
import { type ReactNode } from "react";
|
|
1
|
+
import { Component, type ReactNode } from "react";
|
|
2
2
|
import type { EntitySpriteConfig, ModelConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
import { type FallbackSeam } from "@jgengine/core/devtools/fallbackSeams";
|
|
3
4
|
export declare function EntitySprite({ sprite }: {
|
|
4
5
|
sprite: EntitySpriteConfig;
|
|
5
6
|
}): import("react").JSX.Element;
|
|
7
|
+
/**
|
|
8
|
+
* Catches a render throw from a model subtree and swaps in `fallback` — but never silently: the
|
|
9
|
+
* failed state re-reports to the fallback-seam probe on every render (matching the per-seam frame
|
|
10
|
+
* boundary in `WorldScene`) and the catch warns once with the model url and the error. Without
|
|
11
|
+
* that, a throw anywhere in a composition erases the actor with an empty `probes.fallbacks`.
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export declare class ModelFallbackBoundary extends Component<{
|
|
15
|
+
fallback: ReactNode;
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
url: string;
|
|
18
|
+
seam?: FallbackSeam;
|
|
19
|
+
}, {
|
|
20
|
+
failed: boolean;
|
|
21
|
+
}> {
|
|
22
|
+
state: {
|
|
23
|
+
failed: boolean;
|
|
24
|
+
};
|
|
25
|
+
static getDerivedStateFromError(): {
|
|
26
|
+
failed: boolean;
|
|
27
|
+
};
|
|
28
|
+
componentDidCatch(error: unknown): void;
|
|
29
|
+
render(): ReactNode;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* One model subtree behind its own error boundary and Suspense boundary, so a throwing or
|
|
33
|
+
* still-loading piece cannot erase its siblings. Every nested piece of a composition — parts,
|
|
34
|
+
* attachments, material maps — mounts through this.
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
export declare function IsolatedModelPart({ model, seam, children, }: {
|
|
38
|
+
model: ModelConfig;
|
|
39
|
+
seam?: FallbackSeam;
|
|
40
|
+
children: ReactNode;
|
|
41
|
+
}): import("react").JSX.Element;
|
|
6
42
|
export declare function IsolatedEntityModel({ model, instanceId, measure, fallback, }: {
|
|
7
43
|
model: ModelConfig;
|
|
8
44
|
instanceId?: string;
|