@jgengine/shell 0.13.0 → 0.15.0-next.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 +529 -1
- package/dist/CombatPresentation.js +2 -1
- package/dist/GamePlayerShell.js +25 -5
- package/dist/Shell3dPresentation.d.ts +6 -6
- package/dist/Shell3dPresentation.js +4 -4
- 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 +16 -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 +15 -1
- package/dist/devtools/DevtoolsOverlay.js +2 -0
- package/dist/devtools/collisionDebugMath.d.ts +8 -0
- package/dist/devtools/collisionDebugMath.js +14 -5
- 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 +38 -5
- package/dist/postfx/ScreenEffectsOverlay.d.ts +32 -0
- package/dist/postfx/ScreenEffectsOverlay.js +70 -0
- package/dist/postfx/postfxOverlay.d.ts +24 -0
- package/dist/postfx/postfxOverlay.js +35 -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/EntityPreview.d.ts +53 -0
- package/dist/render/EntityPreview.js +23 -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 +52 -10
- package/dist/render/measureBounds.js +4 -0
- package/dist/render/measureCollisionMesh.d.ts +26 -0
- package/dist/render/measureCollisionMesh.js +108 -0
- 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 +7 -3
- 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 +8 -3
- package/skills/jgengine-ui/api.md +732 -23
- package/skills/jgengine-ui/capabilities.md +371 -2
- 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 +408 -12
- package/skills/jgengine-world/capabilities.md +127 -2
- package/skills/jgengine-world/reference.md +15 -1
package/dist/inputSink.js
CHANGED
|
@@ -77,6 +77,20 @@ export function inputFramesEqual(a, b) {
|
|
|
77
77
|
for (let i = 0; i < a.held.length; i++)
|
|
78
78
|
if (a.held[i] !== b.held[i])
|
|
79
79
|
return false;
|
|
80
|
+
const aa = a.analog ?? null;
|
|
81
|
+
const ba = b.analog ?? null;
|
|
82
|
+
if (aa === null || ba === null) {
|
|
83
|
+
if (aa !== ba)
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const keys = Object.keys(aa);
|
|
88
|
+
if (keys.length !== Object.keys(ba).length)
|
|
89
|
+
return false;
|
|
90
|
+
for (const key of keys)
|
|
91
|
+
if (aa[key] !== ba[key])
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
80
94
|
const pa = a.pointer;
|
|
81
95
|
const pb = b.pointer;
|
|
82
96
|
if (pa === null || pb === null)
|
package/dist/materialOverride.js
CHANGED
|
@@ -36,5 +36,35 @@ function overrideOne(material, override, clone, textures) {
|
|
|
36
36
|
target.aoMap = textures.ao;
|
|
37
37
|
if (textures !== undefined)
|
|
38
38
|
target.needsUpdate = true;
|
|
39
|
+
if (override.rim !== undefined)
|
|
40
|
+
applyRimLight(target, override.rim);
|
|
39
41
|
return target;
|
|
40
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Adds a fresnel rim term to a standard material's outgoing light. Patched through
|
|
45
|
+
* `onBeforeCompile` rather than as a second pass so the rim shadows, fogs, and tonemaps with the
|
|
46
|
+
* body it belongs to instead of floating in front of it.
|
|
47
|
+
*/
|
|
48
|
+
function applyRimLight(target, rim) {
|
|
49
|
+
const color = new THREE.Color(rim.color ?? "#ffffff");
|
|
50
|
+
const strength = rim.strength ?? 0.6;
|
|
51
|
+
const power = rim.power ?? 2.5;
|
|
52
|
+
if (strength <= 0)
|
|
53
|
+
return;
|
|
54
|
+
target.onBeforeCompile = (shader) => {
|
|
55
|
+
shader.uniforms.uJgRimColor = { value: color };
|
|
56
|
+
shader.uniforms.uJgRimStrength = { value: strength };
|
|
57
|
+
shader.uniforms.uJgRimPower = { value: power };
|
|
58
|
+
shader.fragmentShader = shader.fragmentShader
|
|
59
|
+
.replace("#include <common>", `#include <common>
|
|
60
|
+
uniform vec3 uJgRimColor;
|
|
61
|
+
uniform float uJgRimStrength;
|
|
62
|
+
uniform float uJgRimPower;`)
|
|
63
|
+
.replace("#include <opaque_fragment>", `float jgRim = pow(1.0 - clamp(dot(normalize(normal), normalize(vViewPosition)), 0.0, 1.0), uJgRimPower);
|
|
64
|
+
outgoingLight += uJgRimColor * jgRim * uJgRimStrength;
|
|
65
|
+
#include <opaque_fragment>`);
|
|
66
|
+
};
|
|
67
|
+
// Two materials differing only in rim tuning must not share a compiled program.
|
|
68
|
+
target.customProgramCacheKey = () => `jgengine-rim-${rim.color ?? "#ffffff"}-${strength}-${power}`;
|
|
69
|
+
target.needsUpdate = true;
|
|
70
|
+
}
|
|
@@ -3,14 +3,15 @@ import type { GraphicsQuality } from "@jgengine/core/settings/settingsModel";
|
|
|
3
3
|
/**
|
|
4
4
|
* Mounts an `EffectComposer` inside the shell Canvas and takes over rendering
|
|
5
5
|
* (priority-1 `useFrame`, which disables R3F auto-render) to run the configured
|
|
6
|
-
* post chain: RenderPass → GTAO → UnrealBloom → OutputPass → Grade. Rendered only
|
|
6
|
+
* post chain: RenderPass → GTAO → UnrealBloom → SMAA → OutputPass → Grade. Rendered only
|
|
7
7
|
* when `PlayableGame.postProcessing` is set, so games without it draw unchanged.
|
|
8
8
|
*
|
|
9
9
|
* `quality` (the player's graphics-quality setting) gates the passes whose cost
|
|
10
10
|
* scales with scene geometry or resolution beyond the dpr cap: GTAO re-renders
|
|
11
11
|
* the whole scene for depth/normals and runs a multi-sample full-screen pass,
|
|
12
12
|
* and Bokeh DOF renders scene depth again — both run on "high" only. Bloom,
|
|
13
|
-
* tone mapping, and grade stay on every tier
|
|
13
|
+
* SMAA, tone mapping, and grade stay on every tier (SMAA is the cheap edge fix
|
|
14
|
+
* for alpha-tested foliage that MSAA samples alone leave crawling).
|
|
14
15
|
*/
|
|
15
16
|
export declare function PostProcessing({ config, quality }: {
|
|
16
17
|
config: PostProcessingConfig;
|
|
@@ -5,7 +5,33 @@ 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";
|
|
10
|
+
import { hidePostfxOverlays, restorePostfxOverlays } from "./postfxOverlay.js";
|
|
11
|
+
/**
|
|
12
|
+
* GTAO renders the scene with an `overrideMaterial` normal/depth prepass, which stamps
|
|
13
|
+
* transparent overlay quads (additive VFX sprites, telegraphs, debug gizmos) into the AO
|
|
14
|
+
* buffers as opaque squares — the AO composite then multiplies them toward black. Hide
|
|
15
|
+
* marked overlay subtrees for the whole pass: the beauty buffer it composites over was
|
|
16
|
+
* already rendered with them in.
|
|
17
|
+
*/
|
|
18
|
+
class OverlayAwareGTAOPass extends GTAOPass {
|
|
19
|
+
hiddenOverlays = [];
|
|
20
|
+
render(renderer, writeBuffer, readBuffer, deltaTime, maskActive) {
|
|
21
|
+
hidePostfxOverlays(this.scene, this.hiddenOverlays);
|
|
22
|
+
super.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive);
|
|
23
|
+
restorePostfxOverlays(this.hiddenOverlays);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** DOF's depth prepass has the same `overrideMaterial` overlay-stamping problem as GTAO. */
|
|
27
|
+
class OverlayAwareBokehPass extends BokehPass {
|
|
28
|
+
hiddenOverlays = [];
|
|
29
|
+
render(renderer, writeBuffer, readBuffer, deltaTime, maskActive) {
|
|
30
|
+
hidePostfxOverlays(this.scene, this.hiddenOverlays);
|
|
31
|
+
super.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive);
|
|
32
|
+
restorePostfxOverlays(this.hiddenOverlays);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
9
35
|
const TONE_MAPPING = {
|
|
10
36
|
aces: THREE.ACESFilmicToneMapping,
|
|
11
37
|
agx: THREE.AgXToneMapping,
|
|
@@ -34,14 +60,15 @@ function syncSize(built, width, height, pixelRatio) {
|
|
|
34
60
|
/**
|
|
35
61
|
* Mounts an `EffectComposer` inside the shell Canvas and takes over rendering
|
|
36
62
|
* (priority-1 `useFrame`, which disables R3F auto-render) to run the configured
|
|
37
|
-
* post chain: RenderPass → GTAO → UnrealBloom → OutputPass → Grade. Rendered only
|
|
63
|
+
* post chain: RenderPass → GTAO → UnrealBloom → SMAA → OutputPass → Grade. Rendered only
|
|
38
64
|
* when `PlayableGame.postProcessing` is set, so games without it draw unchanged.
|
|
39
65
|
*
|
|
40
66
|
* `quality` (the player's graphics-quality setting) gates the passes whose cost
|
|
41
67
|
* scales with scene geometry or resolution beyond the dpr cap: GTAO re-renders
|
|
42
68
|
* the whole scene for depth/normals and runs a multi-sample full-screen pass,
|
|
43
69
|
* and Bokeh DOF renders scene depth again — both run on "high" only. Bloom,
|
|
44
|
-
* 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).
|
|
45
72
|
*/
|
|
46
73
|
export function PostProcessing({ config, quality = "high" }) {
|
|
47
74
|
const gl = useThree((s) => s.gl);
|
|
@@ -52,15 +79,17 @@ export function PostProcessing({ config, quality = "high" }) {
|
|
|
52
79
|
useEffect(() => {
|
|
53
80
|
const width = Math.max(1, size.width);
|
|
54
81
|
const height = Math.max(1, size.height);
|
|
82
|
+
const aa = config.aa === undefined ? "smaa" : config.aa;
|
|
83
|
+
const msaaSamples = aa === false ? 0 : 2;
|
|
55
84
|
const target = new THREE.WebGLRenderTarget(width, height, {
|
|
56
85
|
type: THREE.HalfFloatType,
|
|
57
|
-
samples:
|
|
86
|
+
samples: msaaSamples,
|
|
58
87
|
});
|
|
59
88
|
const composer = new EffectComposer(gl, target);
|
|
60
89
|
composer.addPass(new RenderPass(scene, camera));
|
|
61
90
|
const heavyPasses = quality === "high";
|
|
62
91
|
if (heavyPasses && config.ao !== undefined && config.ao !== false) {
|
|
63
|
-
const ao = new
|
|
92
|
+
const ao = new OverlayAwareGTAOPass(scene, camera, width, height);
|
|
64
93
|
ao.blendIntensity = config.ao.blend ?? 1;
|
|
65
94
|
ao.updateGtaoMaterial({
|
|
66
95
|
radius: config.ao.radius ?? 1.8,
|
|
@@ -78,13 +107,17 @@ export function PostProcessing({ config, quality = "high" }) {
|
|
|
78
107
|
let dof = null;
|
|
79
108
|
if (heavyPasses && config.dof !== undefined && config.dof !== false) {
|
|
80
109
|
const d = config.dof;
|
|
81
|
-
dof = new
|
|
110
|
+
dof = new OverlayAwareBokehPass(scene, camera, {
|
|
82
111
|
focus: d.focus ?? 18,
|
|
83
112
|
aperture: d.aperture ?? 0.00025,
|
|
84
113
|
maxblur: d.maxBlur ?? 0.01,
|
|
85
114
|
});
|
|
86
115
|
composer.addPass(dof);
|
|
87
116
|
}
|
|
117
|
+
// SMAA must run before OutputPass (linear-sRGB). Default on for post chains.
|
|
118
|
+
if (aa === "smaa") {
|
|
119
|
+
composer.addPass(new SMAAPass());
|
|
120
|
+
}
|
|
88
121
|
composer.addPass(new OutputPass());
|
|
89
122
|
let grade = null;
|
|
90
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
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type * as THREE from "three";
|
|
2
|
+
/**
|
|
3
|
+
* Spread onto a group's `userData` to mark its subtree as a screen/world overlay effect —
|
|
4
|
+
* additive glows, telegraphs, tracers, debug gizmos — that post-processing scene prepasses
|
|
5
|
+
* (GTAO's normal/depth render, DOF's depth render) must skip. Those prepasses render the
|
|
6
|
+
* scene with `overrideMaterial`, which turns transparent overlay quads into opaque stamps
|
|
7
|
+
* in the AO/DOF buffers (visible as black squares over hit sparks). The shell marks its own
|
|
8
|
+
* combat VFX and collision-debug mounts; games mark custom overlay effects the same way.
|
|
9
|
+
*/
|
|
10
|
+
export declare const POSTFX_OVERLAY_USERDATA: {
|
|
11
|
+
readonly jgPostfxOverlay: true;
|
|
12
|
+
};
|
|
13
|
+
/** True when the object opted out of postfx scene prepasses via {@link POSTFX_OVERLAY_USERDATA}. */
|
|
14
|
+
export declare function isPostfxOverlay(object: {
|
|
15
|
+
userData?: Record<string, unknown>;
|
|
16
|
+
}): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Hide every currently visible marked object under `scene`, recording what was hidden into
|
|
19
|
+
* `out` (cleared first, no allocation on the hot path) so {@link restorePostfxOverlays} can
|
|
20
|
+
* undo exactly that set.
|
|
21
|
+
*/
|
|
22
|
+
export declare function hidePostfxOverlays(scene: THREE.Object3D, out: THREE.Object3D[]): void;
|
|
23
|
+
/** Restore visibility for the objects hidden by {@link hidePostfxOverlays} and clear the list. */
|
|
24
|
+
export declare function restorePostfxOverlays(hidden: THREE.Object3D[]): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spread onto a group's `userData` to mark its subtree as a screen/world overlay effect —
|
|
3
|
+
* additive glows, telegraphs, tracers, debug gizmos — that post-processing scene prepasses
|
|
4
|
+
* (GTAO's normal/depth render, DOF's depth render) must skip. Those prepasses render the
|
|
5
|
+
* scene with `overrideMaterial`, which turns transparent overlay quads into opaque stamps
|
|
6
|
+
* in the AO/DOF buffers (visible as black squares over hit sparks). The shell marks its own
|
|
7
|
+
* combat VFX and collision-debug mounts; games mark custom overlay effects the same way.
|
|
8
|
+
*/
|
|
9
|
+
export const POSTFX_OVERLAY_USERDATA = {
|
|
10
|
+
jgPostfxOverlay: true,
|
|
11
|
+
};
|
|
12
|
+
/** True when the object opted out of postfx scene prepasses via {@link POSTFX_OVERLAY_USERDATA}. */
|
|
13
|
+
export function isPostfxOverlay(object) {
|
|
14
|
+
return object.userData?.jgPostfxOverlay === true;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Hide every currently visible marked object under `scene`, recording what was hidden into
|
|
18
|
+
* `out` (cleared first, no allocation on the hot path) so {@link restorePostfxOverlays} can
|
|
19
|
+
* undo exactly that set.
|
|
20
|
+
*/
|
|
21
|
+
export function hidePostfxOverlays(scene, out) {
|
|
22
|
+
out.length = 0;
|
|
23
|
+
scene.traverse((object) => {
|
|
24
|
+
if (object.visible && isPostfxOverlay(object)) {
|
|
25
|
+
object.visible = false;
|
|
26
|
+
out.push(object);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** Restore visibility for the objects hidden by {@link hidePostfxOverlays} and clear the list. */
|
|
31
|
+
export function restorePostfxOverlays(hidden) {
|
|
32
|
+
for (const object of hidden)
|
|
33
|
+
object.visible = true;
|
|
34
|
+
hidden.length = 0;
|
|
35
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import type { ModelConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
import { type StudioMood } from "../scene/StudioStage.js";
|
|
4
|
+
/** Props for {@link EntityPreview}. */
|
|
5
|
+
export interface EntityPreviewProps {
|
|
6
|
+
/**
|
|
7
|
+
* The rendered entity — the game's *own* `renderEntity` content (e.g. its procedural body component),
|
|
8
|
+
* which may call `useEntityRenderCues(id)` / `useGameContext()` internally to mirror the live game.
|
|
9
|
+
* Takes precedence over `model`. Omit both to render an empty lit stage.
|
|
10
|
+
*/
|
|
11
|
+
children?: ReactNode;
|
|
12
|
+
/**
|
|
13
|
+
* Convenience for engine-native models: render this {@link ModelConfig} via `IsolatedEntityModel`.
|
|
14
|
+
* Pair with `instanceId` to bind it to a live entity so the preview walks / flinches / topples in
|
|
15
|
+
* sync with the world. Ignored when `children` is supplied.
|
|
16
|
+
*/
|
|
17
|
+
model?: ModelConfig;
|
|
18
|
+
/** Live entity id driving motion cues for `model` (also lets a custom child resolve the same entity). */
|
|
19
|
+
instanceId?: string;
|
|
20
|
+
/** Lighting mood for the stage. Default `"studio"`. */
|
|
21
|
+
mood?: StudioMood;
|
|
22
|
+
/** Override the backdrop color (hex). Default: the mood's backdrop. */
|
|
23
|
+
backdrop?: string;
|
|
24
|
+
/** Turntable spin (rad/s). Default 0 — a still portrait that mirrors the live entity's own motion. */
|
|
25
|
+
turntable?: number;
|
|
26
|
+
/** Yaw the subject to face the camera each frame instead of spinning. Overrides `turntable`. */
|
|
27
|
+
faceCamera?: boolean;
|
|
28
|
+
/** The subject's declared forward axis for `faceCamera`. Default `[0, 0, 1]`. */
|
|
29
|
+
forward?: readonly [number, number, number];
|
|
30
|
+
/** Draw the backdrop + floor (true) or keep a transparent stage as a pure lighting rig (false). Default true. */
|
|
31
|
+
environment?: boolean;
|
|
32
|
+
/** Camera position framing the subject. Default `[0, 1, 3]`. */
|
|
33
|
+
cameraPosition?: [number, number, number];
|
|
34
|
+
/** Camera field of view. Default 35. */
|
|
35
|
+
fov?: number;
|
|
36
|
+
/** Device pixel ratio clamp. Default `[1, 2]`. */
|
|
37
|
+
dpr?: number | [number, number];
|
|
38
|
+
className?: string;
|
|
39
|
+
style?: CSSProperties;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Drop-in live 3D preview of a single entity — the character-screen portrait, unit inspector, or loadout
|
|
43
|
+
* viewer. It owns all the plumbing a game would otherwise re-derive by hand: a nested `<Canvas>`, the
|
|
44
|
+
* {@link GameContextBridge} that carries the running `GameContext` across the R3F reconciler boundary (so
|
|
45
|
+
* the subject's `useEntityRenderCues` / `useGameContext` hooks resolve and it mirrors the live game), a
|
|
46
|
+
* {@link StudioStage} 3-point lighting rig with optional turntable / face-camera, and a capture-friendly
|
|
47
|
+
* GL surface. The game keeps ownership of its `renderEntity`: pass it as `children`, or hand a native
|
|
48
|
+
* `model` (+ `instanceId`) for an engine-composed body. Bound to a live `instanceId`, the portrait walks,
|
|
49
|
+
* flinches, and topples in lockstep with the world instead of a canned spin.
|
|
50
|
+
*
|
|
51
|
+
* @capability entity-preview live 3D entity portrait — nested Canvas + context bridge + studio stage, driven by the game's own renderEntity
|
|
52
|
+
*/
|
|
53
|
+
export declare function EntityPreview({ children, model, instanceId, mood, backdrop, turntable, faceCamera, forward, environment, cameraPosition, fov, dpr, className, style, }: EntityPreviewProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Canvas } from "@react-three/fiber";
|
|
3
|
+
import { GameContextBridge, useOptionalGameContext } from "@jgengine/react/provider";
|
|
4
|
+
import { StudioStage } from "../scene/StudioStage.js";
|
|
5
|
+
import { IsolatedEntityModel } from "./SceneModels.js";
|
|
6
|
+
/**
|
|
7
|
+
* Drop-in live 3D preview of a single entity — the character-screen portrait, unit inspector, or loadout
|
|
8
|
+
* viewer. It owns all the plumbing a game would otherwise re-derive by hand: a nested `<Canvas>`, the
|
|
9
|
+
* {@link GameContextBridge} that carries the running `GameContext` across the R3F reconciler boundary (so
|
|
10
|
+
* the subject's `useEntityRenderCues` / `useGameContext` hooks resolve and it mirrors the live game), a
|
|
11
|
+
* {@link StudioStage} 3-point lighting rig with optional turntable / face-camera, and a capture-friendly
|
|
12
|
+
* GL surface. The game keeps ownership of its `renderEntity`: pass it as `children`, or hand a native
|
|
13
|
+
* `model` (+ `instanceId`) for an engine-composed body. Bound to a live `instanceId`, the portrait walks,
|
|
14
|
+
* flinches, and topples in lockstep with the world instead of a canned spin.
|
|
15
|
+
*
|
|
16
|
+
* @capability entity-preview live 3D entity portrait — nested Canvas + context bridge + studio stage, driven by the game's own renderEntity
|
|
17
|
+
*/
|
|
18
|
+
export function EntityPreview({ children, model, instanceId, mood = "studio", backdrop, turntable = 0, faceCamera = false, forward, environment = true, cameraPosition = [0, 1, 3], fov = 35, dpr = [1, 2], className, style, }) {
|
|
19
|
+
const ctx = useOptionalGameContext();
|
|
20
|
+
const subject = children ??
|
|
21
|
+
(model !== undefined ? _jsx(IsolatedEntityModel, { model: model, instanceId: instanceId }) : null);
|
|
22
|
+
return (_jsx("div", { "data-jg-entity-preview": "", className: className, style: { position: "relative", width: "100%", height: "100%", ...style }, children: _jsx(Canvas, { shadows: true, dpr: dpr, camera: { position: cameraPosition, fov }, gl: { preserveDrawingBuffer: true }, style: { touchAction: "none" }, children: _jsx(GameContextBridge, { context: ctx, children: _jsx(StudioStage, { mood: mood, ...(backdrop === undefined ? {} : { backdrop }), turntable: turntable, faceCamera: faceCamera, ...(forward === undefined ? {} : { forward }), environment: environment, children: subject }) }) }) }));
|
|
23
|
+
}
|
|
@@ -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;
|