@jgengine/shell 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +265 -2
- package/dist/GamePlayerShell.js +27 -4
- package/dist/JoinGate.d.ts +17 -0
- package/dist/JoinGate.js +14 -0
- package/dist/Shell3dPresentation.d.ts +9 -8
- package/dist/Shell3dPresentation.js +57 -8
- package/dist/ShellChrome.d.ts +2 -0
- package/dist/ShellChrome.js +6 -0
- package/dist/ShellHudPresentation.d.ts +9 -1
- package/dist/ShellHudPresentation.js +21 -0
- package/dist/TerritoryOverlay.d.ts +21 -0
- package/dist/TerritoryOverlay.js +9 -0
- package/dist/audio/AudioComponents.js +13 -2
- package/dist/audio/audioEngine.d.ts +15 -1
- package/dist/audio/audioEngine.js +132 -11
- package/dist/audio/audioWire.d.ts +3 -0
- package/dist/audio/audioWire.js +7 -1
- package/dist/audio/loopParams.d.ts +39 -0
- package/dist/audio/loopParams.js +43 -0
- package/dist/camera/GameCameraRig.js +1 -1
- package/dist/camera/GameFirstPersonCamera.d.ts +4 -2
- package/dist/camera/GameFirstPersonCamera.js +52 -15
- package/dist/camera/Viewports.d.ts +34 -0
- package/dist/camera/Viewports.js +123 -0
- package/dist/camera/cameraRigs.js +77 -62
- package/dist/camera/orbitCameraMath.js +2 -2
- package/dist/camera/rigMath.d.ts +82 -4
- package/dist/camera/rigMath.js +133 -15
- package/dist/defineGame.d.ts +9 -0
- package/dist/defineGame.js +38 -6
- package/dist/devtools/DevtoolsOverlay.js +2 -0
- package/dist/devtools/KeysPanel.js +3 -3
- package/dist/devtools/agentBridge.d.ts +6 -0
- package/dist/devtools/agentBridge.js +15 -1
- package/dist/diagnostics/FpsCounter.d.ts +17 -0
- package/dist/diagnostics/FpsCounter.js +32 -0
- package/dist/drivers/FrameDriver.d.ts +5 -1
- package/dist/drivers/FrameDriver.js +74 -26
- package/dist/drivers/FrameRateLimiter.d.ts +9 -0
- package/dist/drivers/FrameRateLimiter.js +39 -0
- package/dist/drivers/HudOnlyDriver.js +10 -5
- package/dist/environment/DayNightSky.js +5 -5
- package/dist/environment/Daylight.d.ts +35 -1
- package/dist/environment/Daylight.js +98 -28
- package/dist/environment/EnvironmentScene.js +1 -1
- package/dist/environment/VolumetricClouds.js +20 -2
- package/dist/environment/daylightCycle.d.ts +20 -0
- package/dist/environment/daylightCycle.js +59 -7
- package/dist/environment/sunShadowMath.d.ts +45 -0
- package/dist/environment/sunShadowMath.js +50 -0
- package/dist/input/gamepadMerge.d.ts +10 -0
- package/dist/input/gamepadMerge.js +12 -0
- package/dist/input/gamepadPoll.d.ts +47 -0
- package/dist/input/gamepadPoll.js +142 -0
- package/dist/input/gamepadSource.d.ts +22 -0
- package/dist/input/gamepadSource.js +153 -0
- package/dist/input/mouseLook.js +2 -1
- package/dist/input/padHaptics.d.ts +17 -0
- package/dist/input/padHaptics.js +32 -0
- package/dist/input/pointerLock.d.ts +11 -0
- package/dist/input/pointerLock.js +19 -0
- package/dist/materialOverride.d.ts +4 -1
- package/dist/materialOverride.js +10 -1
- package/dist/postfx/PostProcessing.d.ts +11 -8
- package/dist/postfx/PostProcessing.js +38 -16
- package/dist/postfx/gradeShader.js +13 -0
- package/dist/postfx/stylizePass.d.ts +23 -0
- package/dist/postfx/stylizePass.js +216 -0
- package/dist/render/CascadedShadows.d.ts +11 -0
- package/dist/render/CascadedShadows.js +19 -8
- package/dist/render/EnvironmentLighting.d.ts +4 -1
- package/dist/render/EnvironmentLighting.js +45 -9
- package/dist/render/SceneLighting.d.ts +16 -2
- package/dist/render/SceneLighting.js +56 -6
- package/dist/render/SceneModels.js +45 -9
- package/dist/render/SkinnedInstances.d.ts +91 -0
- package/dist/render/SkinnedInstances.js +249 -0
- package/dist/render/TextureFiltering.d.ts +16 -0
- package/dist/render/TextureFiltering.js +64 -0
- package/dist/render/displayColor.d.ts +33 -0
- package/dist/render/displayColor.js +138 -0
- package/dist/render/modelLoad.d.ts +39 -0
- package/dist/render/modelLoad.js +89 -1
- package/dist/render/modelRender.d.ts +3 -0
- package/dist/render/modelRender.js +6 -2
- package/dist/render/resolveModel.js +4 -0
- package/dist/render/useFootIk.d.ts +58 -0
- package/dist/render/useFootIk.js +216 -0
- package/dist/render/useModelAnimation.d.ts +26 -0
- package/dist/render/useModelAnimation.js +184 -107
- package/dist/scene/AuthoredScene.d.ts +9 -0
- package/dist/scene/AuthoredScene.js +17 -1
- package/dist/scene/CityRenderer.js +666 -241
- package/dist/scene/cityWindowMaterial.d.ts +8 -6
- package/dist/scene/cityWindowMaterial.js +80 -21
- package/dist/settings/QuickControls.js +1 -1
- package/dist/settings/SettingsMenu.js +1 -1
- package/dist/settings/appliedSettings.d.ts +10 -2
- package/dist/settings/appliedSettings.js +9 -5
- package/dist/settings/settingsController.d.ts +3 -0
- package/dist/settings/settingsController.js +47 -3
- package/dist/structures/BuildingKitBatch.js +26 -22
- package/dist/structures/GeneratedBuilding.d.ts +3 -15
- package/dist/structures/GeneratedBuilding.js +21 -15
- package/dist/structures/buildingSurface.d.ts +21 -0
- package/dist/structures/buildingSurface.js +134 -0
- package/dist/terrain/terrainDetailMaterial.js +7 -3
- package/dist/useShellMultiplayerSync.d.ts +9 -2
- package/dist/useShellMultiplayerSync.js +30 -8
- package/dist/visibility/CullingProvider.d.ts +2 -1
- package/dist/visibility/CullingProvider.js +7 -2
- package/dist/world/GridWorldScene.js +4 -0
- package/dist/world/TileLayerRenderer.d.ts +9 -0
- package/dist/world/TileLayerRenderer.js +64 -0
- package/dist/world/WorldParticles.d.ts +31 -0
- package/dist/world/WorldParticles.js +128 -0
- package/dist/world/WorldScene.d.ts +2 -1
- package/dist/world/WorldScene.js +46 -4
- package/dist/world/entityPose.d.ts +4 -0
- package/dist/world/entityPose.js +9 -0
- package/dist/world/tileLayerInstances.d.ts +11 -0
- package/dist/world/tileLayerInstances.js +28 -0
- package/dist/worldSync.d.ts +5 -1
- package/dist/worldSync.js +25 -4
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +2 -2
- package/skills/game-design/SKILL.md +11 -1
- package/skills/jgengine/SKILL.md +9 -5
- package/skills/jgengine/api.md +205 -101
- package/skills/jgengine/capabilities.md +56 -0
- package/skills/jgengine/recipes/minimal-game.md +3 -0
- package/skills/jgengine-ui/SKILL.md +20 -15
- package/skills/jgengine-ui/api.md +538 -326
- package/skills/jgengine-ui/capabilities.md +37 -0
- package/skills/jgengine-ui/reference-primitives.md +89 -0
- package/skills/jgengine-ui/reference.md +2 -78
- package/skills/jgengine-world/SKILL.md +21 -3
- package/skills/jgengine-world/api.md +1172 -625
- package/skills/jgengine-world/capabilities.md +229 -3
- package/skills/jgengine-world/recipes/character-animation.md +39 -0
- package/skills/jgengine-world/recipes/controls.md +44 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +43 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
- package/skills/jgengine-world/reference.md +31 -2
- package/skills/jgengine-world/visibility.md +112 -0
- package/skills/level-design/SKILL.md +1 -1
- package/skills/jgengine-ui/references/visual-scorecard.md +0 -73
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useThree } from "@react-three/fiber";
|
|
2
|
+
import { useLayoutEffect } from "react";
|
|
3
|
+
import * as THREE from "three";
|
|
4
|
+
const TEXTURE_SLOTS = [
|
|
5
|
+
"map",
|
|
6
|
+
"normalMap",
|
|
7
|
+
"roughnessMap",
|
|
8
|
+
"metalnessMap",
|
|
9
|
+
"aoMap",
|
|
10
|
+
"emissiveMap",
|
|
11
|
+
"bumpMap",
|
|
12
|
+
"alphaMap",
|
|
13
|
+
"displacementMap",
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Retune already-loaded textures that still carry the old default anisotropy. Textures a
|
|
17
|
+
* system set on purpose (pixel-art sprites pin 1) keep their value.
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export function retuneTextureAnisotropy(root, from, to) {
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
root.traverse((node) => {
|
|
23
|
+
const mesh = node;
|
|
24
|
+
if (mesh.material === undefined)
|
|
25
|
+
return;
|
|
26
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
27
|
+
for (const material of materials) {
|
|
28
|
+
const slots = material;
|
|
29
|
+
for (const slot of TEXTURE_SLOTS) {
|
|
30
|
+
const texture = slots[slot];
|
|
31
|
+
if (texture === null || texture === undefined || seen.has(texture))
|
|
32
|
+
continue;
|
|
33
|
+
seen.add(texture);
|
|
34
|
+
if (texture.anisotropy !== from)
|
|
35
|
+
continue;
|
|
36
|
+
texture.anisotropy = to;
|
|
37
|
+
texture.needsUpdate = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
return seen.size;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Applies the player's texture-filtering choice. Sets three's construction-time default so
|
|
45
|
+
* every texture loaded afterwards (GLTF, texture loaders) samples anisotropically, and walks the
|
|
46
|
+
* scene once per change for textures loaded before. No per-frame work.
|
|
47
|
+
* @internal shell-internal; driven by `GraphicsProfile.anisotropy`.
|
|
48
|
+
*/
|
|
49
|
+
export function TextureFiltering({ anisotropy }) {
|
|
50
|
+
const gl = useThree((state) => state.gl);
|
|
51
|
+
const scene = useThree((state) => state.scene);
|
|
52
|
+
useLayoutEffect(() => {
|
|
53
|
+
const target = Math.max(1, Math.min(anisotropy, gl.capabilities.getMaxAnisotropy()));
|
|
54
|
+
const previous = THREE.Texture.DEFAULT_ANISOTROPY;
|
|
55
|
+
if (previous === target)
|
|
56
|
+
return;
|
|
57
|
+
THREE.Texture.DEFAULT_ANISOTROPY = target;
|
|
58
|
+
retuneTextureAnisotropy(scene, previous, target);
|
|
59
|
+
return () => {
|
|
60
|
+
THREE.Texture.DEFAULT_ANISOTROPY = previous;
|
|
61
|
+
};
|
|
62
|
+
}, [gl, scene, anisotropy]);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type Color, type ToneMapping } from "three";
|
|
2
|
+
type Rgb = [number, number, number];
|
|
3
|
+
/**
|
|
4
|
+
* three.js r182's tone-mapping curve for `mode`, applied to a linear scene color at `exposure`.
|
|
5
|
+
* Mirrors `tonemapping_pars_fragment`; `NoToneMapping` and custom curves pass through.
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export declare function toneMapColor(color: readonly [number, number, number], mode: ToneMapping, exposure?: number): Rgb;
|
|
9
|
+
/**
|
|
10
|
+
* The linear scene color that `mode` at `exposure` displays as `display` (linear sRGB, 0..1).
|
|
11
|
+
* Sky and fog colors are picked as on-screen swatches, but the output pass tone-maps them like
|
|
12
|
+
* lit surfaces, which turns a pale horizon grey. Solved by fixed-point iteration because AgX and
|
|
13
|
+
* ACES mix channels; a swatch a curve cannot reach (pure white, a saturated highlight under AgX)
|
|
14
|
+
* lands on the closest color it can show.
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export declare function sceneColorForDisplay(display: readonly [number, number, number], mode: ToneMapping, exposure?: number): Rgb;
|
|
18
|
+
/**
|
|
19
|
+
* Writes the scene color that displays as `display` into `target`, recomputing only when the
|
|
20
|
+
* swatch or the renderer's tone mapping changes, so per-frame sky drivers stay allocation-free.
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export declare class DisplayColorWriter {
|
|
24
|
+
private hex;
|
|
25
|
+
private mode;
|
|
26
|
+
private exposure;
|
|
27
|
+
private scene;
|
|
28
|
+
write(target: Color, display: Color, renderer: {
|
|
29
|
+
toneMapping: ToneMapping;
|
|
30
|
+
toneMappingExposure: number;
|
|
31
|
+
}): void;
|
|
32
|
+
}
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { ACESFilmicToneMapping, AgXToneMapping, CineonToneMapping, LinearToneMapping, NeutralToneMapping, ReinhardToneMapping, } from "three";
|
|
2
|
+
const saturate = (x) => Math.min(1, Math.max(0, x));
|
|
3
|
+
/** GLSL `mat3(c0, c1, c2) * v` with the columns three's tonemapping chunk declares. */
|
|
4
|
+
function mat3(c, v) {
|
|
5
|
+
return [
|
|
6
|
+
c[0][0] * v[0] + c[1][0] * v[1] + c[2][0] * v[2],
|
|
7
|
+
c[0][1] * v[0] + c[1][1] * v[1] + c[2][1] * v[2],
|
|
8
|
+
c[0][2] * v[0] + c[1][2] * v[1] + c[2][2] * v[2],
|
|
9
|
+
];
|
|
10
|
+
}
|
|
11
|
+
const ACES_IN = [[0.59719, 0.076, 0.0284], [0.35458, 0.90834, 0.13383], [0.04823, 0.01566, 0.83777]];
|
|
12
|
+
const ACES_OUT = [[1.60475, -0.10208, -0.00327], [-0.53108, 1.10813, -0.07276], [-0.07367, -0.00605, 1.07602]];
|
|
13
|
+
const REC2020_TO_SRGB = [[1.6605, -0.1246, -0.0182], [-0.5876, 1.1329, -0.1006], [-0.0728, -0.0083, 1.1187]];
|
|
14
|
+
const SRGB_TO_REC2020 = [[0.6274, 0.0691, 0.0164], [0.3293, 0.9195, 0.088], [0.0433, 0.0113, 0.8956]];
|
|
15
|
+
const AGX_INSET = [
|
|
16
|
+
[0.856627153315983, 0.137318972929847, 0.11189821299995],
|
|
17
|
+
[0.0951212405381588, 0.761241990602591, 0.0767994186031903],
|
|
18
|
+
[0.0482516061458583, 0.101439036467562, 0.811302368396859],
|
|
19
|
+
];
|
|
20
|
+
const AGX_OUTSET = [
|
|
21
|
+
[1.1271005818144368, -0.1413297634984383, -0.14132976349843826],
|
|
22
|
+
[-0.11060664309660323, 1.157823702216272, -0.11060664309660294],
|
|
23
|
+
[-0.016493938717834573, -0.016493938717834257, 1.2519364065950405],
|
|
24
|
+
];
|
|
25
|
+
const AGX_MIN_EV = -12.47393;
|
|
26
|
+
const AGX_MAX_EV = 4.026069;
|
|
27
|
+
function aces(c) {
|
|
28
|
+
const v = mat3(ACES_IN, [c[0] / 0.6, c[1] / 0.6, c[2] / 0.6]);
|
|
29
|
+
const fit = v.map((x) => (x * (x + 0.0245786) - 0.000090537) / (x * (0.983729 * x + 0.432951) + 0.238081));
|
|
30
|
+
return mat3(ACES_OUT, fit).map(saturate);
|
|
31
|
+
}
|
|
32
|
+
function agx(c) {
|
|
33
|
+
const inset = mat3(AGX_INSET, mat3(SRGB_TO_REC2020, c));
|
|
34
|
+
const curved = inset.map((x) => {
|
|
35
|
+
const t = saturate((Math.log2(Math.max(x, 1e-10)) - AGX_MIN_EV) / (AGX_MAX_EV - AGX_MIN_EV));
|
|
36
|
+
const t2 = t * t;
|
|
37
|
+
const t4 = t2 * t2;
|
|
38
|
+
return 15.5 * t4 * t2 - 40.14 * t4 * t + 31.96 * t4 - 6.868 * t2 * t + 0.4298 * t2 + 0.1191 * t - 0.00232;
|
|
39
|
+
});
|
|
40
|
+
const out = mat3(AGX_OUTSET, curved).map((x) => Math.pow(Math.max(0, x), 2.2));
|
|
41
|
+
return mat3(REC2020_TO_SRGB, out).map(saturate);
|
|
42
|
+
}
|
|
43
|
+
function neutral(input) {
|
|
44
|
+
const x = Math.min(input[0], input[1], input[2]);
|
|
45
|
+
const offset = x < 0.08 ? x - 6.25 * x * x : 0.04;
|
|
46
|
+
const c = input.map((v) => v - offset);
|
|
47
|
+
const peak = Math.max(c[0], c[1], c[2]);
|
|
48
|
+
const start = 0.76;
|
|
49
|
+
if (peak < start)
|
|
50
|
+
return c;
|
|
51
|
+
const d = 1 - start;
|
|
52
|
+
const newPeak = 1 - (d * d) / (peak + d - start);
|
|
53
|
+
const g = 1 - 1 / (0.15 * (peak - newPeak) + 1);
|
|
54
|
+
return c.map((v) => v * (newPeak / peak) * (1 - g) + newPeak * g);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* three.js r182's tone-mapping curve for `mode`, applied to a linear scene color at `exposure`.
|
|
58
|
+
* Mirrors `tonemapping_pars_fragment`; `NoToneMapping` and custom curves pass through.
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
export function toneMapColor(color, mode, exposure = 1) {
|
|
62
|
+
const c = [color[0] * exposure, color[1] * exposure, color[2] * exposure];
|
|
63
|
+
switch (mode) {
|
|
64
|
+
case LinearToneMapping:
|
|
65
|
+
return c.map(saturate);
|
|
66
|
+
case ReinhardToneMapping:
|
|
67
|
+
return c.map((x) => saturate(x / (1 + x)));
|
|
68
|
+
case CineonToneMapping:
|
|
69
|
+
return c.map((v) => {
|
|
70
|
+
const x = Math.max(0, v - 0.004);
|
|
71
|
+
return Math.pow((x * (6.2 * x + 0.5)) / (x * (6.2 * x + 1.7) + 0.06), 2.2);
|
|
72
|
+
});
|
|
73
|
+
case ACESFilmicToneMapping:
|
|
74
|
+
return aces(c);
|
|
75
|
+
case AgXToneMapping:
|
|
76
|
+
return agx(c);
|
|
77
|
+
case NeutralToneMapping:
|
|
78
|
+
return neutral(c);
|
|
79
|
+
default:
|
|
80
|
+
return c;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const CURVED = new Set([ReinhardToneMapping, CineonToneMapping, ACESFilmicToneMapping, AgXToneMapping, NeutralToneMapping]);
|
|
84
|
+
/** Keeps a compensated sky inside half-float range; the brightest pastel under AgX needs ~13. */
|
|
85
|
+
const MAX_SCENE_VALUE = 32;
|
|
86
|
+
/**
|
|
87
|
+
* The linear scene color that `mode` at `exposure` displays as `display` (linear sRGB, 0..1).
|
|
88
|
+
* Sky and fog colors are picked as on-screen swatches, but the output pass tone-maps them like
|
|
89
|
+
* lit surfaces, which turns a pale horizon grey. Solved by fixed-point iteration because AgX and
|
|
90
|
+
* ACES mix channels; a swatch a curve cannot reach (pure white, a saturated highlight under AgX)
|
|
91
|
+
* lands on the closest color it can show.
|
|
92
|
+
* @internal
|
|
93
|
+
*/
|
|
94
|
+
export function sceneColorForDisplay(display, mode, exposure = 1) {
|
|
95
|
+
const target = [saturate(display[0]), saturate(display[1]), saturate(display[2])];
|
|
96
|
+
if (mode === LinearToneMapping)
|
|
97
|
+
return target.map((v) => v / Math.max(exposure, 1e-6));
|
|
98
|
+
if (!CURVED.has(mode))
|
|
99
|
+
return target;
|
|
100
|
+
const scene = [...target];
|
|
101
|
+
let best = [...target];
|
|
102
|
+
let bestError = Number.POSITIVE_INFINITY;
|
|
103
|
+
for (let iteration = 0; iteration < 48; iteration += 1) {
|
|
104
|
+
const shown = toneMapColor(scene, mode, exposure);
|
|
105
|
+
const error = Math.abs(shown[0] - target[0]) + Math.abs(shown[1] - target[1]) + Math.abs(shown[2] - target[2]);
|
|
106
|
+
if (error < bestError) {
|
|
107
|
+
bestError = error;
|
|
108
|
+
best = [scene[0], scene[1], scene[2]];
|
|
109
|
+
}
|
|
110
|
+
for (let channel = 0; channel < 3; channel += 1) {
|
|
111
|
+
// Clamped so a channel a curve's toe or shoulder cannot reach does not blow up its neighbors.
|
|
112
|
+
const ratio = Math.min(2, Math.max(0.5, Math.max(target[channel], 1e-5) / Math.max(shown[channel], 1e-5)));
|
|
113
|
+
scene[channel] = Math.min(MAX_SCENE_VALUE, Math.max(1e-5, scene[channel] * Math.pow(ratio, 0.8)));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return best;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Writes the scene color that displays as `display` into `target`, recomputing only when the
|
|
120
|
+
* swatch or the renderer's tone mapping changes, so per-frame sky drivers stay allocation-free.
|
|
121
|
+
* @internal
|
|
122
|
+
*/
|
|
123
|
+
export class DisplayColorWriter {
|
|
124
|
+
hex = -1;
|
|
125
|
+
mode = -1;
|
|
126
|
+
exposure = Number.NaN;
|
|
127
|
+
scene = [0, 0, 0];
|
|
128
|
+
write(target, display, renderer) {
|
|
129
|
+
const hex = display.getHex();
|
|
130
|
+
if (hex !== this.hex || renderer.toneMapping !== this.mode || renderer.toneMappingExposure !== this.exposure) {
|
|
131
|
+
this.hex = hex;
|
|
132
|
+
this.mode = renderer.toneMapping;
|
|
133
|
+
this.exposure = renderer.toneMappingExposure;
|
|
134
|
+
this.scene = sceneColorForDisplay([display.r, display.g, display.b], this.mode, this.exposure);
|
|
135
|
+
}
|
|
136
|
+
target.setRGB(this.scene[0], this.scene[1], this.scene[2]);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
1
2
|
import { GLTFLoader, type GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
2
3
|
import { type AssetLoadDiagnosis } from "@jgengine/core/scene/assetDiagnostics";
|
|
3
4
|
/**
|
|
@@ -60,6 +61,35 @@ export declare function modelLoadFallbacks(): readonly {
|
|
|
60
61
|
}[];
|
|
61
62
|
/** Drops the recorded placeholder list. For tests and between capture runs. @internal */
|
|
62
63
|
export declare function clearModelLoadFallbacks(): void;
|
|
64
|
+
/** Per-model triangle ceiling before the loader warns. A prop is a few thousand triangles; a hero rig tens of thousands. */
|
|
65
|
+
export declare const DEFAULT_MODEL_TRIANGLE_BUDGET = 100000;
|
|
66
|
+
/** Retune the per-model triangle budget (a cinematic game may raise it; a mobile target lowers it). Non-finite or non-positive values restore the default. */
|
|
67
|
+
export declare function setModelTriangleBudget(triangles: number): void;
|
|
68
|
+
/** Triangles a loaded model contributes per draw, summing every mesh (instanced meshes count once per instance). @internal */
|
|
69
|
+
export declare function countSceneTriangles(root: THREE.Object3D): number;
|
|
70
|
+
/** One over-budget model as {@link heavyModels} reports it: the URL the loader fetched, its triangle count, and the budget it exceeded. */
|
|
71
|
+
export interface HeavyModelRecord {
|
|
72
|
+
url: string;
|
|
73
|
+
triangles: number;
|
|
74
|
+
budget: number;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Record a loaded model's triangle count against the budget: over budget warns once per URL and lands in
|
|
78
|
+
* {@link heavyModels}; under budget is silent. Split out so the wiring is unit-testable without a GL context.
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
export declare function recordModelTriangles(url: string, triangles: number, overrides?: {
|
|
82
|
+
warn?: (record: HeavyModelRecord) => void;
|
|
83
|
+
}): void;
|
|
84
|
+
/**
|
|
85
|
+
* Every model the shared loader resolved whose triangle count exceeds the budget, in load order. A heavy
|
|
86
|
+
* prop is invisible in a screenshot and only shows up as a slow frame, so the loader measures each GLB as it
|
|
87
|
+
* lands and this exposes the verdict for `debug_snapshot` probes and smoke tests to assert against.
|
|
88
|
+
* @capability heavy-models models over the per-model triangle budget, for perf probes and smoke assertions
|
|
89
|
+
*/
|
|
90
|
+
export declare function heavyModels(): readonly HeavyModelRecord[];
|
|
91
|
+
/** Drops the heavy-model list and the once-per-URL warning memory. For tests and between capture runs. @internal */
|
|
92
|
+
export declare function clearHeavyModels(): void;
|
|
63
93
|
/**
|
|
64
94
|
* A {@link GLTFLoader} whose success path is unchanged but whose failures degrade
|
|
65
95
|
* gracefully: when a load errors, it probes the URL and — for a diagnosed broken
|
|
@@ -72,6 +102,15 @@ export declare function clearModelLoadFallbacks(): void;
|
|
|
72
102
|
export declare class DiagnosticGLTFLoader extends GLTFLoader {
|
|
73
103
|
load(url: string, onLoad: (gltf: GLTF) => void, onProgress?: (event: ProgressEvent) => void, onError?: (event: unknown) => void): void;
|
|
74
104
|
}
|
|
105
|
+
/** CDN-backed decoder configuration for compressed model and texture assets. */
|
|
106
|
+
export interface ModelLoaderConfig {
|
|
107
|
+
dracoDecoderPath?: string;
|
|
108
|
+
ktx2TranscoderPath?: string;
|
|
109
|
+
}
|
|
110
|
+
/** Configures shared Draco and KTX2 loaders. Repeated calls with the same paths are no-ops. */
|
|
111
|
+
export declare function configureModelLoaders(options?: ModelLoaderConfig): Readonly<Required<ModelLoaderConfig>>;
|
|
112
|
+
/** Detects GPU support for the configured KTX2 transcoder after a renderer exists. */
|
|
113
|
+
export declare function detectKtx2Support(renderer: THREE.WebGLRenderer): void;
|
|
75
114
|
/**
|
|
76
115
|
* How long the shared GLB loader has been idle, in ms — `0` while any model is still
|
|
77
116
|
* in flight. A capture host reads this to wait for streaming to finish instead of
|
package/dist/render/modelLoad.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
3
|
+
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
|
|
4
|
+
import { KTX2Loader } from "three/examples/jsm/loaders/KTX2Loader.js";
|
|
3
5
|
import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js";
|
|
4
6
|
import { classifyAssetResponse, } from "@jgengine/core/scene/assetDiagnostics";
|
|
5
7
|
import { reportTextureLoadError } from "@jgengine/core/devtools/textureErrors";
|
|
@@ -138,6 +140,63 @@ export function modelLoadFallbacks() {
|
|
|
138
140
|
export function clearModelLoadFallbacks() {
|
|
139
141
|
servedFallbacks.length = 0;
|
|
140
142
|
}
|
|
143
|
+
/** Per-model triangle ceiling before the loader warns. A prop is a few thousand triangles; a hero rig tens of thousands. */
|
|
144
|
+
export const DEFAULT_MODEL_TRIANGLE_BUDGET = 100_000;
|
|
145
|
+
let modelTriangleBudget = DEFAULT_MODEL_TRIANGLE_BUDGET;
|
|
146
|
+
/** Retune the per-model triangle budget (a cinematic game may raise it; a mobile target lowers it). Non-finite or non-positive values restore the default. */
|
|
147
|
+
export function setModelTriangleBudget(triangles) {
|
|
148
|
+
modelTriangleBudget = Number.isFinite(triangles) && triangles > 0 ? triangles : DEFAULT_MODEL_TRIANGLE_BUDGET;
|
|
149
|
+
}
|
|
150
|
+
/** Triangles a loaded model contributes per draw, summing every mesh (instanced meshes count once per instance). @internal */
|
|
151
|
+
export function countSceneTriangles(root) {
|
|
152
|
+
let total = 0;
|
|
153
|
+
root.traverse((node) => {
|
|
154
|
+
const mesh = node;
|
|
155
|
+
if (mesh.isMesh !== true || mesh.geometry === undefined)
|
|
156
|
+
return;
|
|
157
|
+
const geometry = mesh.geometry;
|
|
158
|
+
const vertices = geometry.index !== null ? geometry.index.count : geometry.attributes.position?.count ?? 0;
|
|
159
|
+
const instances = mesh.isInstancedMesh === true && typeof mesh.count === "number" ? mesh.count : 1;
|
|
160
|
+
total += Math.floor(vertices / 3) * instances;
|
|
161
|
+
});
|
|
162
|
+
return total;
|
|
163
|
+
}
|
|
164
|
+
const heavyModelRecords = [];
|
|
165
|
+
const warnedHeavyUrls = new Set();
|
|
166
|
+
function warnHeavyModel(record) {
|
|
167
|
+
if (typeof console === "undefined")
|
|
168
|
+
return;
|
|
169
|
+
console.warn(`[jgengine] model ${record.url} has ${record.triangles.toLocaleString()} triangles (budget ${record.budget.toLocaleString()}). A game prop should be a few thousand — pick a lighter asset or decimate it.`);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Record a loaded model's triangle count against the budget: over budget warns once per URL and lands in
|
|
173
|
+
* {@link heavyModels}; under budget is silent. Split out so the wiring is unit-testable without a GL context.
|
|
174
|
+
* @internal
|
|
175
|
+
*/
|
|
176
|
+
export function recordModelTriangles(url, triangles, overrides = {}) {
|
|
177
|
+
if (triangles <= modelTriangleBudget)
|
|
178
|
+
return;
|
|
179
|
+
const record = { url, triangles, budget: modelTriangleBudget };
|
|
180
|
+
heavyModelRecords.push(record);
|
|
181
|
+
if (warnedHeavyUrls.has(url))
|
|
182
|
+
return;
|
|
183
|
+
warnedHeavyUrls.add(url);
|
|
184
|
+
(overrides.warn ?? warnHeavyModel)(record);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Every model the shared loader resolved whose triangle count exceeds the budget, in load order. A heavy
|
|
188
|
+
* prop is invisible in a screenshot and only shows up as a slow frame, so the loader measures each GLB as it
|
|
189
|
+
* lands and this exposes the verdict for `debug_snapshot` probes and smoke tests to assert against.
|
|
190
|
+
* @capability heavy-models models over the per-model triangle budget, for perf probes and smoke assertions
|
|
191
|
+
*/
|
|
192
|
+
export function heavyModels() {
|
|
193
|
+
return heavyModelRecords;
|
|
194
|
+
}
|
|
195
|
+
/** Drops the heavy-model list and the once-per-URL warning memory. For tests and between capture runs. @internal */
|
|
196
|
+
export function clearHeavyModels() {
|
|
197
|
+
heavyModelRecords.length = 0;
|
|
198
|
+
warnedHeavyUrls.clear();
|
|
199
|
+
}
|
|
141
200
|
/**
|
|
142
201
|
* A {@link GLTFLoader} whose success path is unchanged but whose failures degrade
|
|
143
202
|
* gracefully: when a load errors, it probes the URL and — for a diagnosed broken
|
|
@@ -149,7 +208,10 @@ export function clearModelLoadFallbacks() {
|
|
|
149
208
|
*/
|
|
150
209
|
export class DiagnosticGLTFLoader extends GLTFLoader {
|
|
151
210
|
load(url, onLoad, onProgress, onError) {
|
|
152
|
-
super.load(url,
|
|
211
|
+
super.load(url, (gltf) => {
|
|
212
|
+
recordModelTriangles(url, countSceneTriangles(gltf.scene));
|
|
213
|
+
onLoad(gltf);
|
|
214
|
+
}, onProgress, (event) => {
|
|
153
215
|
if (onError === undefined)
|
|
154
216
|
return;
|
|
155
217
|
// Probe the URL the loader actually fetched (base-resolved), not the raw input.
|
|
@@ -157,6 +219,31 @@ export class DiagnosticGLTFLoader extends GLTFLoader {
|
|
|
157
219
|
});
|
|
158
220
|
}
|
|
159
221
|
}
|
|
222
|
+
const DEFAULT_DRACO_DECODER_PATH = "https://cdn.jsdelivr.net/npm/three@0.182.0/examples/jsm/libs/draco/";
|
|
223
|
+
const DEFAULT_KTX2_TRANSCODER_PATH = "https://cdn.jsdelivr.net/npm/three@0.182.0/examples/jsm/libs/basis/";
|
|
224
|
+
let configuredModelLoaders;
|
|
225
|
+
let ktx2Loader;
|
|
226
|
+
/** Configures shared Draco and KTX2 loaders. Repeated calls with the same paths are no-ops. */
|
|
227
|
+
export function configureModelLoaders(options = {}) {
|
|
228
|
+
const next = {
|
|
229
|
+
dracoDecoderPath: options.dracoDecoderPath ?? DEFAULT_DRACO_DECODER_PATH,
|
|
230
|
+
ktx2TranscoderPath: options.ktx2TranscoderPath ?? DEFAULT_KTX2_TRANSCODER_PATH,
|
|
231
|
+
};
|
|
232
|
+
if (configuredModelLoaders?.dracoDecoderPath === next.dracoDecoderPath && configuredModelLoaders.ktx2TranscoderPath === next.ktx2TranscoderPath) {
|
|
233
|
+
return configuredModelLoaders;
|
|
234
|
+
}
|
|
235
|
+
const draco = new DRACOLoader().setDecoderPath(next.dracoDecoderPath);
|
|
236
|
+
ktx2Loader = new KTX2Loader().setTranscoderPath(next.ktx2TranscoderPath);
|
|
237
|
+
sharedGltfLoader.setDRACOLoader(draco);
|
|
238
|
+
sharedGltfLoader.setKTX2Loader(ktx2Loader);
|
|
239
|
+
configuredModelLoaders = next;
|
|
240
|
+
return configuredModelLoaders;
|
|
241
|
+
}
|
|
242
|
+
/** Detects GPU support for the configured KTX2 transcoder after a renderer exists. */
|
|
243
|
+
export function detectKtx2Support(renderer) {
|
|
244
|
+
configureModelLoaders();
|
|
245
|
+
ktx2Loader?.detectSupport(renderer);
|
|
246
|
+
}
|
|
160
247
|
// LoadingManager reports queue transitions, not per-item events: `onStart` fires when the
|
|
161
248
|
// queue goes busy and `onLoad` when it drains (errors included, via itemEnd), so a flag plus
|
|
162
249
|
// a drain timestamp is the whole state.
|
|
@@ -187,3 +274,4 @@ export function modelLoadIdleMs() {
|
|
|
187
274
|
/** Shared GLTF loader used by shell entity/object model mounts. @internal */
|
|
188
275
|
export const sharedGltfLoader = new DiagnosticGLTFLoader(modelLoadingManager);
|
|
189
276
|
sharedGltfLoader.setMeshoptDecoder(MeshoptDecoder);
|
|
277
|
+
configureModelLoaders();
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
import type { PaintStroke } from "@jgengine/core/scene/paintLayer";
|
|
3
3
|
export declare const PAINT_TEXTURE_SIZE = 512;
|
|
4
|
+
/** Shadow participation applied to every mesh of a cloned model; mirrors `ModelConfig.shadows`. */
|
|
5
|
+
export type ModelShadowMode = "cast" | "receive" | "both" | "none";
|
|
4
6
|
/** @internal */
|
|
5
7
|
export declare function cloneModelScene(source: THREE.Object3D, options?: {
|
|
6
8
|
cloneMaterials?: boolean;
|
|
9
|
+
shadows?: ModelShadowMode;
|
|
7
10
|
}): THREE.Object3D;
|
|
8
11
|
/** @internal */
|
|
9
12
|
export declare function disposeClonedMaterials(root: THREE.Object3D): void;
|
|
@@ -4,12 +4,16 @@ export const PAINT_TEXTURE_SIZE = 512;
|
|
|
4
4
|
/** @internal */
|
|
5
5
|
export function cloneModelScene(source, options) {
|
|
6
6
|
const clone = cloneSkinned(source);
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const shadows = options?.shadows ?? "both";
|
|
8
|
+
const cloneMaterials = options?.cloneMaterials !== false;
|
|
9
9
|
clone.traverse((node) => {
|
|
10
10
|
const mesh = node;
|
|
11
11
|
if (!mesh.isMesh)
|
|
12
12
|
return;
|
|
13
|
+
mesh.castShadow = shadows === "cast" || shadows === "both";
|
|
14
|
+
mesh.receiveShadow = shadows === "receive" || shadows === "both";
|
|
15
|
+
if (!cloneMaterials)
|
|
16
|
+
return;
|
|
13
17
|
mesh.material = Array.isArray(mesh.material)
|
|
14
18
|
? mesh.material.map((material) => material.clone())
|
|
15
19
|
: mesh.material.clone();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { warnOnce } from "@jgengine/core/devtools/warnOnce";
|
|
1
2
|
/**
|
|
2
3
|
* The `ModelConfig` a resolved catalog ref renders as. Stamps `animation: "auto"` so any rigged
|
|
3
4
|
* catalog asset animates by default — the shell derives idle/walk/run states and one-shots from
|
|
@@ -74,6 +75,9 @@ export function resolveEntityModel(value, assets, key) {
|
|
|
74
75
|
* @internal
|
|
75
76
|
*/
|
|
76
77
|
export function pickModel(assets, pick) {
|
|
78
|
+
if (pick.fallbackModel !== undefined && !assets.has(pick.fallbackModel)) {
|
|
79
|
+
warnOnce(`pickModel:fallback:${pick.fallbackModel}`, `[jgengine] fallbackModel "${pick.fallbackModel}" is not in the asset catalog, so it can never stand in for "${pick.model ?? "(none)"}" — fix the id or declare its pack in the catalog sources.`);
|
|
80
|
+
}
|
|
77
81
|
for (const id of [pick.model, pick.fallbackModel]) {
|
|
78
82
|
if (id === undefined)
|
|
79
83
|
continue;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type { GameContext } from "@jgengine/core/runtime/gameContextTypes";
|
|
3
|
+
import type { ModelConfig } from "@jgengine/core/game/playableGame";
|
|
4
|
+
import type { SceneRaycastHit, SceneRaycastInput } from "@jgengine/core/scene/sceneRaycast";
|
|
5
|
+
type FootIkConfig = NonNullable<ModelConfig["ik"]>;
|
|
6
|
+
type GroundProbe = (input: SceneRaycastInput) => SceneRaycastHit | null;
|
|
7
|
+
interface IkLeg {
|
|
8
|
+
root: THREE.Object3D;
|
|
9
|
+
mid: THREE.Object3D;
|
|
10
|
+
tip: THREE.Object3D;
|
|
11
|
+
/** Rest-pose ankle height above the sole divided by rest leg length, so it survives any scale. */
|
|
12
|
+
ankleRatio: number;
|
|
13
|
+
}
|
|
14
|
+
interface PoseRecord {
|
|
15
|
+
bone: THREE.Object3D;
|
|
16
|
+
animated: {
|
|
17
|
+
q: THREE.Quaternion;
|
|
18
|
+
p: THREE.Vector3;
|
|
19
|
+
};
|
|
20
|
+
written: {
|
|
21
|
+
q: THREE.Quaternion;
|
|
22
|
+
p: THREE.Vector3;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Bones resolved once per loaded scene for {@link applyFootIk}. */
|
|
26
|
+
export interface FootIkRig {
|
|
27
|
+
legs: IkLeg[];
|
|
28
|
+
pelvis: THREE.Object3D | null;
|
|
29
|
+
head: THREE.Object3D | null;
|
|
30
|
+
maxAdjust: number;
|
|
31
|
+
alignToGround: number;
|
|
32
|
+
/** Pose before and after last frame's correction, so a bone no clip animates never accumulates it. */
|
|
33
|
+
poses: PoseRecord[];
|
|
34
|
+
}
|
|
35
|
+
/** Per-model state carried between frames: the faded weight and the smoothed pelvis drop. */
|
|
36
|
+
export interface FootIkState {
|
|
37
|
+
weight: number;
|
|
38
|
+
pelvis: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolves foot-IK bones on a loaded rig: explicit chains, or legs found by bone name for `"auto"`
|
|
42
|
+
* and configs without `feet`. Returns `null` when no leg resolves.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveFootIkRig(scene: THREE.Object3D, config: FootIkConfig): FootIkRig | null;
|
|
45
|
+
/**
|
|
46
|
+
* Applies one frame of foot IK after the animation mixer: probes the ground under each foot,
|
|
47
|
+
* resolves targets with `placeFeet`, lowers the pelvis, solves each leg with `solveTwoBone` bending
|
|
48
|
+
* toward the animated knee, and tilts planted feet to the ground. Returns whether the feet are
|
|
49
|
+
* grounded; `state` carries the faded weight between frames. Exported for tests and custom hosts.
|
|
50
|
+
*/
|
|
51
|
+
export declare function applyFootIk(rig: FootIkRig, originY: number, probe: GroundProbe, state: FootIkState, delta: number, cameraTarget?: readonly [number, number, number]): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Runs foot IK after the model's animation mixer for `ModelConfig.ik`. Ground probes hit terrain and
|
|
54
|
+
* blocking physical objects, never the model's own entity. `groundOffset` is the model-space height
|
|
55
|
+
* its soles rest on (`ModelConfig.y`).
|
|
56
|
+
*/
|
|
57
|
+
export declare function useFootIk(scene: THREE.Object3D, config: FootIkConfig | undefined, ctx: GameContext | null, instanceId?: string, groundOffset?: number): void;
|
|
58
|
+
export {};
|