@jgengine/shell 0.21.0 → 0.23.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 +114 -0
- package/dist/GamePlayerShell.js +18 -3
- package/dist/GameUiPreview.js +2 -1
- package/dist/Shell3dPresentation.js +13 -8
- package/dist/ShellChrome.d.ts +1 -1
- package/dist/ShellChrome.js +4 -2
- package/dist/ShellHudPresentation.js +2 -1
- package/dist/ShellNotices.d.ts +26 -0
- package/dist/ShellNotices.js +51 -0
- package/dist/audio/AudioComponents.d.ts +3 -1
- package/dist/audio/AudioComponents.js +5 -2
- package/dist/audio/listenerAnchor.d.ts +15 -0
- package/dist/audio/listenerAnchor.js +17 -0
- package/dist/audio/playableAudio.d.ts +9 -0
- package/dist/audio/playableAudio.js +11 -0
- package/dist/commandSink.d.ts +1 -1
- package/dist/commandSink.js +8 -3
- package/dist/defineGame.js +4 -2
- package/dist/devtools/DevtoolsOverlay.js +1 -0
- package/dist/devtools/agentBridge.d.ts +21 -0
- package/dist/devtools/agentBridge.js +95 -3
- package/dist/devtools/devtoolsReports.d.ts +32 -0
- package/dist/devtools/devtoolsReports.js +26 -0
- package/dist/devtools/simStepper.d.ts +21 -0
- package/dist/devtools/simStepper.js +37 -0
- package/dist/drivers/FrameDriver.js +26 -14
- package/dist/drivers/HudOnlyDriver.js +16 -5
- package/dist/environment/EnvironmentScene.js +24 -9
- package/dist/environment/RoadJunctions.d.ts +3 -1
- package/dist/environment/RoadJunctions.js +9 -2
- package/dist/environment/RoadRibbons.d.ts +3 -1
- package/dist/environment/RoadRibbons.js +16 -3
- package/dist/environment/terrainGroundColor.d.ts +5 -4
- package/dist/environment/terrainGroundColor.js +20 -9
- package/dist/materialOverride.js +4 -2
- package/dist/playSurfaceFocus.d.ts +16 -0
- package/dist/playSurfaceFocus.js +42 -0
- package/dist/postfx/PostProcessing.d.ts +3 -1
- package/dist/postfx/PostProcessing.js +14 -6
- package/dist/registry.d.ts +5 -1
- package/dist/render/CascadedShadows.d.ts +2 -0
- package/dist/render/CascadedShadows.js +4 -0
- package/dist/render/EntityPreview.js +1 -1
- package/dist/render/EnvironmentLighting.d.ts +26 -7
- package/dist/render/EnvironmentLighting.js +40 -27
- package/dist/render/SceneLighting.js +2 -2
- package/dist/render/modelLoad.d.ts +7 -0
- package/dist/render/modelLoad.js +13 -0
- package/dist/render/modelRender.d.ts +1 -0
- package/dist/render/modelRender.js +3 -0
- package/dist/render/modelSurface.d.ts +39 -0
- package/dist/render/modelSurface.js +206 -0
- package/dist/render/useModelInstance.d.ts +1 -1
- package/dist/render/useModelInstance.js +2 -2
- package/dist/scatter/scatterModels.js +3 -1
- package/dist/scene/AuthoredScene.d.ts +11 -3
- package/dist/scene/AuthoredScene.js +17 -9
- package/dist/scene/CityRenderer.js +86 -24
- package/dist/scene/GrassFieldRenderer.js +20 -2
- package/dist/scene/cityJunctionSurface.d.ts +7 -0
- package/dist/scene/cityJunctionSurface.js +34 -0
- package/dist/settings/settingsController.js +1 -1
- package/dist/terrain/CarvedTerrain.d.ts +2 -1
- package/dist/terrain/CarvedTerrain.js +2 -2
- package/dist/terrain/GrassField.d.ts +2 -1
- package/dist/terrain/GrassField.js +3 -2
- package/dist/terrain/grassGeometry.d.ts +3 -0
- package/dist/terrain/grassGeometry.js +2 -1
- package/dist/terrain/index.d.ts +5 -0
- package/dist/terrain/index.js +5 -0
- package/dist/terrain/surfaceRibbonMaterial.d.ts +62 -0
- package/dist/terrain/surfaceRibbonMaterial.js +227 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +33 -10
- package/dist/terrain/terrainDetailMaterial.js +149 -63
- package/dist/terrain/terrainMath.d.ts +2 -0
- package/dist/terrain/terrainMath.js +2 -0
- package/dist/terrain/terrainPaintColor.d.ts +20 -0
- package/dist/terrain/terrainPaintColor.js +92 -0
- package/dist/terrain/terrainQuality.d.ts +5 -0
- package/dist/terrain/terrainQuality.js +7 -0
- package/dist/terrain/terrainSurfaceShader.d.ts +16 -0
- package/dist/terrain/terrainSurfaceShader.js +266 -0
- package/dist/terrain/terrainSurfaces.d.ts +59 -0
- package/dist/terrain/terrainSurfaces.js +199 -0
- package/dist/world/StaticObjectModels.d.ts +1 -1
- package/dist/world/StaticObjectModels.js +1 -1
- package/dist/world/WorldScene.js +1 -1
- package/dist/world/staticObjectBatches.d.ts +5 -1
- package/dist/world/staticObjectBatches.js +9 -5
- package/dist/world/worldBarSamples.d.ts +13 -1
- package/dist/world/worldBarSamples.js +40 -8
- package/package.json +5 -5
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +1 -0
- package/skills/jgengine/api.md +18 -18
- package/skills/jgengine-ui/SKILL.md +4 -0
- package/skills/jgengine-ui/api.md +242 -34
- package/skills/jgengine-ui/capabilities.md +56 -0
- package/skills/jgengine-ui/reference-primitives.md +4 -4
- package/skills/jgengine-world/api.md +28 -15
- package/skills/jgengine-world/capabilities.md +4 -0
- package/skills/jgengine-world/reference.md +30 -0
|
@@ -3,6 +3,8 @@ import { useMemo } from "react";
|
|
|
3
3
|
import { BufferAttribute, BufferGeometry, Color, DoubleSide } from "three";
|
|
4
4
|
import { buildJunctionSurface, buildRoadRibbon, dashSegments, GROUND_DECAL_LAYERS, trimPathAtJunctions, } from "@jgengine/core/world/roads";
|
|
5
5
|
import { offsetPath, sidewalkWidthOf } from "@jgengine/core/world/streets";
|
|
6
|
+
import { resolvePathSurface, RIBBON_FRAME_ATTRIBUTE, ribbonSurfaceFrame, useSurfaceRibbonMaterial, } from "../terrain/surfaceRibbonMaterial.js";
|
|
7
|
+
import { useTerrainQuality } from "../terrain/terrainQuality.js";
|
|
6
8
|
/**
|
|
7
9
|
* Decal sub-layers as small lifts above each road's authored base `elevation`, spaced by the
|
|
8
10
|
* single-owner {@link GROUND_DECAL_LAYERS} table so the edge strip and lane paint never z-fight the
|
|
@@ -47,7 +49,9 @@ function mergeRibbons(ribbons) {
|
|
|
47
49
|
vertexOffset += ribbon.positions.length;
|
|
48
50
|
indexOffset += ribbon.indices.length;
|
|
49
51
|
}
|
|
50
|
-
|
|
52
|
+
const geometry = toGeometry(positions, indices);
|
|
53
|
+
geometry?.setAttribute(RIBBON_FRAME_ATTRIBUTE, new BufferAttribute(ribbonSurfaceFrame(ribbons.map((r) => r.positions)), 3));
|
|
54
|
+
return geometry;
|
|
51
55
|
}
|
|
52
56
|
/** Convert a detected {@link RoadJunction} into the structural {@link RoadJunctionInput} the trimmer wants. @internal */
|
|
53
57
|
export function roadJunctionInput(junction) {
|
|
@@ -150,7 +154,9 @@ export function buildAuthoredRoadNetwork(roads, junctions, sampleHeight) {
|
|
|
150
154
|
* by {@link buildAuthoredRoadNetwork}): an asphalt ribbon, a curb/edge strip along each border, an
|
|
151
155
|
* optional dashed centerline, and optional sidewalks. Because the sub-paths already stop at the
|
|
152
156
|
* junction boundary, every layer naturally ends there — no ribbon ploughs through the crossing and
|
|
153
|
-
* the dashed line no longer needs exclusion circles to skip intersections.
|
|
157
|
+
* the dashed line no longer needs exclusion circles to skip intersections. The carriageway, curbs and
|
|
158
|
+
* sidewalks shade with the terrain's procedural surfaces (`road.surface`, default asphalt with tyre
|
|
159
|
+
* wear); `surface: false` keeps them flat.
|
|
154
160
|
*/
|
|
155
161
|
export function RoadRibbons({ road, field, paths, }) {
|
|
156
162
|
const asphalt = useMemo(() => mergeRibbons(paths.map((path) => buildRoadRibbon(path, road.width, (x, z) => field.sampleHeight(x, z), { elevation: road.elevation }))), [paths, road.width, road.elevation, field]);
|
|
@@ -192,7 +198,14 @@ export function RoadRibbons({ road, field, paths, }) {
|
|
|
192
198
|
}
|
|
193
199
|
return mergeRibbons(ribbons);
|
|
194
200
|
}, [paths, road, field]);
|
|
201
|
+
const quality = useTerrainQuality();
|
|
202
|
+
const detailed = road.surface !== false;
|
|
203
|
+
const style = detailed ? resolvePathSurface("road", road.width, { surface: road.surface ?? "road" }) : null;
|
|
204
|
+
const asphaltMaterial = useSurfaceRibbonMaterial(style === null ? null : { surface: style.surface, wear: style.wear, color: road.color, halfWidth: road.width / 2, quality });
|
|
205
|
+
const curbMaterial = useSurfaceRibbonMaterial(detailed ? { surface: "concrete", color: curbColor, halfWidth: road.width / 2, roughness: 0.9, quality } : null);
|
|
206
|
+
const sidewalkColor = road.sidewalk === false ? "#a7adb8" : road.sidewalk.color;
|
|
207
|
+
const sidewalkMaterial = useSurfaceRibbonMaterial(detailed && sidewalks !== null ? { surface: "pavement", color: sidewalkColor, halfWidth: road.width / 2, roughness: 0.9, scale: 1.6, quality } : null);
|
|
195
208
|
if (asphalt === null)
|
|
196
209
|
return null;
|
|
197
|
-
return (_jsxs("group", { children: [_jsx("mesh", { geometry: asphalt, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: road.color, roughness: 0.95, metalness: 0, side: DoubleSide }) }), curbs !== null ? (_jsx("mesh", { geometry: curbs, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: curbColor, roughness: 0.9, metalness: 0, side: DoubleSide }) })) : null, dashes !== null ? (_jsx("mesh", { geometry: dashes, renderOrder: 1, children: _jsx("meshStandardMaterial", { color: road.markingColor, roughness: 0.8, metalness: 0, side: DoubleSide, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 }) })) : null, sidewalks !== null ? (_jsx("mesh", { geometry: sidewalks, receiveShadow: true,
|
|
210
|
+
return (_jsxs("group", { children: [_jsx("mesh", { geometry: asphalt, receiveShadow: true, ...(asphaltMaterial === null ? {} : { material: asphaltMaterial }), children: asphaltMaterial === null ? _jsx("meshStandardMaterial", { color: road.color, roughness: 0.95, metalness: 0, side: DoubleSide }) : null }), curbs !== null ? (_jsx("mesh", { geometry: curbs, receiveShadow: true, ...(curbMaterial === null ? {} : { material: curbMaterial }), children: curbMaterial === null ? _jsx("meshStandardMaterial", { color: curbColor, roughness: 0.9, metalness: 0, side: DoubleSide }) : null })) : null, dashes !== null ? (_jsx("mesh", { geometry: dashes, renderOrder: 1, children: _jsx("meshStandardMaterial", { color: road.markingColor, roughness: 0.8, metalness: 0, side: DoubleSide, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 }) })) : null, sidewalks !== null ? (_jsx("mesh", { geometry: sidewalks, receiveShadow: true, ...(sidewalkMaterial === null ? {} : { material: sidewalkMaterial }), children: sidewalkMaterial === null ? _jsx("meshStandardMaterial", { color: sidewalkColor, roughness: 0.9, metalness: 0, side: DoubleSide }) : null })) : null] }));
|
|
198
211
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { TerrainEnvironmentDescriptor } from "@jgengine/core/world/features";
|
|
2
2
|
import { type TerrainField } from "@jgengine/core/world/terrain";
|
|
3
|
+
import { type TerraformSnapshot } from "@jgengine/core/world/terraform";
|
|
3
4
|
/**
|
|
4
|
-
* Terrain
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Terrain base color at a world XZ — authored paint first, otherwise the regional palette lerped
|
|
6
|
+
* by ground height. Grass roots and soil-patch borders share the painted color with ground vertices.
|
|
7
|
+
* Descriptor surface mottling and fragment detail are presentation overlays, not sampled here.
|
|
7
8
|
* @internal
|
|
8
9
|
*/
|
|
9
|
-
export declare function terrainGroundColorSampler(terrain: Omit<TerrainEnvironmentDescriptor, "kind"> | undefined, field: TerrainField): ((x: number, z: number) => string) | undefined;
|
|
10
|
+
export declare function terrainGroundColorSampler(terrain: Omit<TerrainEnvironmentDescriptor, "kind"> | undefined, field: TerrainField, sculpt?: TerraformSnapshot): ((x: number, z: number) => string) | undefined;
|
|
@@ -1,22 +1,33 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
|
-
import { resolveTerrainPalette } from "@jgengine/core/world/terrain";
|
|
2
|
+
import { createTerrainPaletteSampler, resolveTerrainPalette } from "@jgengine/core/world/terrain";
|
|
3
|
+
import { editableTerrainFromSnapshot } from "@jgengine/core/world/terraform";
|
|
4
|
+
import { createTerrainPaintColorSampler } from "../terrain/terrainPaintColor.js";
|
|
3
5
|
/**
|
|
4
|
-
* Terrain
|
|
5
|
-
*
|
|
6
|
-
*
|
|
6
|
+
* Terrain base color at a world XZ — authored paint first, otherwise the regional palette lerped
|
|
7
|
+
* by ground height. Grass roots and soil-patch borders share the painted color with ground vertices.
|
|
8
|
+
* Descriptor surface mottling and fragment detail are presentation overlays, not sampled here.
|
|
7
9
|
* @internal
|
|
8
10
|
*/
|
|
9
|
-
export function terrainGroundColorSampler(terrain, field) {
|
|
10
|
-
if (terrain === undefined)
|
|
11
|
+
export function terrainGroundColorSampler(terrain, field, sculpt) {
|
|
12
|
+
if (terrain === undefined && sculpt === undefined)
|
|
11
13
|
return undefined;
|
|
12
14
|
const palette = resolveTerrainPalette(terrain);
|
|
15
|
+
const paletteAt = terrain === undefined ? undefined : createTerrainPaletteSampler(terrain);
|
|
16
|
+
const paintColorAt = sculpt === undefined ? undefined : createTerrainPaintColorSampler(editableTerrainFromSnapshot(sculpt));
|
|
13
17
|
const low = new THREE.Color(palette.low);
|
|
14
18
|
const high = new THREE.Color(palette.high);
|
|
15
|
-
const base = terrain
|
|
16
|
-
const swing = terrain
|
|
19
|
+
const base = terrain?.baseHeight ?? 0;
|
|
20
|
+
const swing = (terrain?.height ?? 0) * 1.2;
|
|
17
21
|
const scratch = new THREE.Color();
|
|
18
22
|
return (x, z) => {
|
|
23
|
+
const regional = paletteAt?.(x, z);
|
|
24
|
+
if (regional !== undefined) {
|
|
25
|
+
low.set(regional.low);
|
|
26
|
+
high.set(regional.high);
|
|
27
|
+
}
|
|
19
28
|
const t = swing <= 0 ? 0.5 : THREE.MathUtils.clamp((field.sampleHeight(x, z) - (base - swing)) / (swing * 2), 0, 1);
|
|
20
|
-
|
|
29
|
+
scratch.copy(low).lerp(high, t);
|
|
30
|
+
paintColorAt?.(x, z, scratch);
|
|
31
|
+
return `#${scratch.getHexString()}`;
|
|
21
32
|
};
|
|
22
33
|
}
|
package/dist/materialOverride.js
CHANGED
|
@@ -117,7 +117,8 @@ function applyRimLight(target, rim) {
|
|
|
117
117
|
if (strength <= 0)
|
|
118
118
|
return;
|
|
119
119
|
const previousCompile = target.onBeforeCompile;
|
|
120
|
-
const previousKey = target.customProgramCacheKey
|
|
120
|
+
const previousKey = target.customProgramCacheKey;
|
|
121
|
+
previousKey.call(target);
|
|
121
122
|
target.onBeforeCompile = (shader, renderer) => {
|
|
122
123
|
previousCompile.call(target, shader, renderer);
|
|
123
124
|
shader.uniforms.uJgRimColor = { value: color };
|
|
@@ -133,6 +134,7 @@ outgoingLight += uJgRimColor * jgRim * uJgRimStrength;
|
|
|
133
134
|
#include <opaque_fragment>`);
|
|
134
135
|
};
|
|
135
136
|
// Two materials differing only in rim tuning must not share a compiled program.
|
|
136
|
-
|
|
137
|
+
// Read the chained key lazily: the default surface's key follows the live graphics tier.
|
|
138
|
+
target.customProgramCacheKey = () => `${previousKey.call(target)}|jgengine-rim-${rim.color ?? "#ffffff"}-${strength}-${power}`;
|
|
137
139
|
target.needsUpdate = true;
|
|
138
140
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focus recovery for the play surface. Gameplay keys are read on the shell's focusable wrapper, so
|
|
3
|
+
* when a focused menu button unmounts (a start screen's "Play", a closed panel's last control) the
|
|
4
|
+
* browser drops focus to `<body>` and WASD silently does nothing until the player clicks the canvas.
|
|
5
|
+
* The one decision: reclaim only when the clicked element is gone from the document and
|
|
6
|
+
* nothing else took focus — a deliberate click elsewhere on the page keeps its focus.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export declare function shouldReclaimPlayFocus(lost: Element | null, active: Element | null, body: Element | null): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Watches clicks (pointer, Enter or Space on a button) and, right after the click's update commits, returns focus to `surface()` if the clicked control unmounted and focus fell to the body.
|
|
12
|
+
* Chromium moves focus to `<body>` on removal without a `focusout`, so the click is the hook.
|
|
13
|
+
* Returns the cleanup.
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
export declare function installPlayFocusReclaim(doc: Document, surface: () => HTMLElement | null): () => void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focus recovery for the play surface. Gameplay keys are read on the shell's focusable wrapper, so
|
|
3
|
+
* when a focused menu button unmounts (a start screen's "Play", a closed panel's last control) the
|
|
4
|
+
* browser drops focus to `<body>` and WASD silently does nothing until the player clicks the canvas.
|
|
5
|
+
* The one decision: reclaim only when the clicked element is gone from the document and
|
|
6
|
+
* nothing else took focus — a deliberate click elsewhere on the page keeps its focus.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export function shouldReclaimPlayFocus(lost, active, body) {
|
|
10
|
+
if (lost === null || lost.isConnected)
|
|
11
|
+
return false;
|
|
12
|
+
return active === null || active === body;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Watches clicks (pointer, Enter or Space on a button) and, right after the click's update commits, returns focus to `surface()` if the clicked control unmounted and focus fell to the body.
|
|
16
|
+
* Chromium moves focus to `<body>` on removal without a `focusout`, so the click is the hook.
|
|
17
|
+
* Returns the cleanup.
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export function installPlayFocusReclaim(doc, surface) {
|
|
21
|
+
let timer;
|
|
22
|
+
const onClick = (event) => {
|
|
23
|
+
const clicked = event.target instanceof Element ? event.target : null;
|
|
24
|
+
if (clicked === null)
|
|
25
|
+
return;
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
// React commits a click's update synchronously at the end of the event; a macrotask is enough and,
|
|
28
|
+
// unlike a frame callback, does not stall when the page renders slowly.
|
|
29
|
+
timer = setTimeout(() => {
|
|
30
|
+
const target = surface();
|
|
31
|
+
if (target === null || !target.isConnected)
|
|
32
|
+
return;
|
|
33
|
+
if (shouldReclaimPlayFocus(clicked, doc.activeElement, doc.body))
|
|
34
|
+
target.focus({ preventScroll: true });
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
doc.addEventListener("click", onClick, true);
|
|
38
|
+
return () => {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
doc.removeEventListener("click", onClick, true);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EffectComposer, type ShaderPass } from "three-stdlib";
|
|
2
|
-
import type
|
|
2
|
+
import { type AoConfig, type PostProcessingConfig } from "@jgengine/core/render/postProcessing";
|
|
3
3
|
import type { GraphicsQuality } from "@jgengine/core/settings/settingsModel";
|
|
4
4
|
interface BuiltGraph {
|
|
5
5
|
composer: EffectComposer;
|
|
@@ -9,6 +9,8 @@ interface BuiltGraph {
|
|
|
9
9
|
export declare function disposeGraph(built: BuiltGraph): void;
|
|
10
10
|
/** Resize the composer and its passes at the renderer's pixel ratio. @internal */
|
|
11
11
|
export declare function syncSize(built: BuiltGraph, width: number, height: number, pixelRatio: number): void;
|
|
12
|
+
/** AO the chain runs: authored config, {@link DEFAULT_AO} when unset, null when off or the tier skips it. @internal */
|
|
13
|
+
export declare function resolveAoStage(ao: PostProcessingConfig["ao"], tierAllowsAo: boolean): AoConfig | null;
|
|
12
14
|
/**
|
|
13
15
|
* Mounts an `EffectComposer` inside the shell Canvas and takes over rendering
|
|
14
16
|
* (priority-1 `useFrame`, which disables R3F auto-render) to run the configured
|
|
@@ -6,6 +6,7 @@ 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
8
|
import { SMAAPass } from "three/examples/jsm/postprocessing/SMAAPass.js";
|
|
9
|
+
import { DEFAULT_AO } from "@jgengine/core/render/postProcessing";
|
|
9
10
|
import { createGradePass } from "./gradeShader.js";
|
|
10
11
|
import { StylizePass } from "./stylizePass.js";
|
|
11
12
|
import { renderShadowlessPrepass } from "./shadowPrepass.js";
|
|
@@ -48,6 +49,12 @@ export function syncSize(built, width, height, pixelRatio) {
|
|
|
48
49
|
built.composer.setPixelRatio(pixelRatio);
|
|
49
50
|
built.composer.setSize(width, height);
|
|
50
51
|
}
|
|
52
|
+
/** AO the chain runs: authored config, {@link DEFAULT_AO} when unset, null when off or the tier skips it. @internal */
|
|
53
|
+
export function resolveAoStage(ao, tierAllowsAo) {
|
|
54
|
+
if (!tierAllowsAo || ao === false)
|
|
55
|
+
return null;
|
|
56
|
+
return ao ?? DEFAULT_AO;
|
|
57
|
+
}
|
|
51
58
|
/**
|
|
52
59
|
* Mounts an `EffectComposer` inside the shell Canvas and takes over rendering
|
|
53
60
|
* (priority-1 `useFrame`, which disables R3F auto-render) to run the configured
|
|
@@ -76,15 +83,16 @@ export function PostProcessing({ config, quality = "high", stages }) {
|
|
|
76
83
|
const composer = new EffectComposer(gl, target);
|
|
77
84
|
composer.addPass(new RenderPass(scene, camera));
|
|
78
85
|
const resolvedStages = stages ?? { ao: quality === "high", bloom: true, dof: quality === "high", smaa: true };
|
|
79
|
-
|
|
86
|
+
const aoStage = resolveAoStage(config.ao, resolvedStages.ao);
|
|
87
|
+
if (aoStage !== null) {
|
|
80
88
|
const ao = new OverlayAwareGTAOPass(scene, camera, width, height);
|
|
81
|
-
ao.blendIntensity =
|
|
89
|
+
ao.blendIntensity = aoStage.blend ?? 1;
|
|
82
90
|
ao.updateGtaoMaterial({
|
|
83
|
-
radius:
|
|
91
|
+
radius: aoStage.radius ?? 1.8,
|
|
84
92
|
distanceExponent: 1,
|
|
85
|
-
thickness:
|
|
86
|
-
scale:
|
|
87
|
-
samples: quality === "
|
|
93
|
+
thickness: aoStage.distanceFalloff ?? 3.6,
|
|
94
|
+
scale: aoStage.intensity ?? 2.4,
|
|
95
|
+
samples: quality === "ultra" ? 16 : quality === "high" ? 12 : 8,
|
|
88
96
|
});
|
|
89
97
|
composer.addPass(ao);
|
|
90
98
|
}
|
package/dist/registry.d.ts
CHANGED
|
@@ -2,10 +2,14 @@ import type { ComponentType, ReactNode } from "react";
|
|
|
2
2
|
import type { PlayableGame as EnginePlayableGame, WorldOverlayProps } from "@jgengine/core/game/playableGame";
|
|
3
3
|
import type { SceneEntity } from "@jgengine/core/scene/entityStore";
|
|
4
4
|
import type { SceneObject } from "@jgengine/core/scene/objectStore";
|
|
5
|
+
import type { HudTheme } from "@jgengine/react/hudTheme";
|
|
5
6
|
import type { ViewmodelProps } from "./camera/GameFirstPersonCamera.js";
|
|
6
7
|
export type RenderEntity = (entity: SceneEntity) => ReactNode;
|
|
7
8
|
export type RenderObject = (object: SceneObject) => ReactNode;
|
|
8
9
|
/** The concrete React-bound playable a shell `defineGame` returns and `GameHost` mounts — the one PlayableGame games handle. */
|
|
9
|
-
export type PlayableGame = EnginePlayableGame<ComponentType, ComponentType, RenderEntity, RenderObject, ComponentType<ViewmodelProps>, ComponentType<WorldOverlayProps
|
|
10
|
+
export type PlayableGame = EnginePlayableGame<ComponentType, ComponentType, RenderEntity, RenderObject, ComponentType<ViewmodelProps>, ComponentType<WorldOverlayProps>> & {
|
|
11
|
+
/** HUD theme the shell applies at the `GameUI` root. Unset uses `defaultHudTheme`; `false` opts out of the default theme and the engine's generic HUD control styling. A theme the game sets inside `GameUI` still wins. */
|
|
12
|
+
hudTheme?: HudTheme | false;
|
|
13
|
+
};
|
|
10
14
|
export type GameRegistry = Record<string, () => Promise<PlayableGame>>;
|
|
11
15
|
export declare function resolveGameLoader(registry: GameRegistry, gameId: string, fallbackGameId?: string): (() => Promise<PlayableGame>) | undefined;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
import { CSM } from "three/examples/jsm/csm/CSM.js";
|
|
3
3
|
import type { DirectionalLightingConfig } from "@jgengine/core/game/playableGame";
|
|
4
|
+
/** @internal Default PCF penumbra in shadow-map texels for configured directional lights. */
|
|
5
|
+
export declare const DEFAULT_SHADOW_RADIUS = 1.25;
|
|
4
6
|
/**
|
|
5
7
|
* Cascaded shadow maps for one directional light — outdoor scenes keep shadows past a
|
|
6
8
|
* single 40-unit ortho frustum. Uses three.js `CSM` (shader injection on standard materials).
|
|
@@ -2,6 +2,8 @@ import { useFrame, useThree } from "@react-three/fiber";
|
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
3
|
import * as THREE from "three";
|
|
4
4
|
import { CSM } from "three/examples/jsm/csm/CSM.js";
|
|
5
|
+
/** @internal Default PCF penumbra in shadow-map texels for configured directional lights. */
|
|
6
|
+
export const DEFAULT_SHADOW_RADIUS = 1.25;
|
|
5
7
|
function isStandardMaterial(mat) {
|
|
6
8
|
return mat instanceof THREE.MeshStandardMaterial || mat instanceof THREE.MeshPhysicalMaterial;
|
|
7
9
|
}
|
|
@@ -41,6 +43,7 @@ export function CascadedShadows({ entry }) {
|
|
|
41
43
|
for (const light of csm.lights) {
|
|
42
44
|
light.color = new THREE.Color(entry.color ?? "#ffffff");
|
|
43
45
|
light.castShadow = true;
|
|
46
|
+
light.shadow.radius = entry.shadowRadius ?? DEFAULT_SHADOW_RADIUS;
|
|
44
47
|
}
|
|
45
48
|
const unbind = bindCsmMaterials(scene, csm);
|
|
46
49
|
csmRef.current = csm;
|
|
@@ -62,6 +65,7 @@ export function CascadedShadows({ entry }) {
|
|
|
62
65
|
entry.shadowCameraSize,
|
|
63
66
|
entry.shadowMapSize,
|
|
64
67
|
entry.shadowMaxFar,
|
|
68
|
+
entry.shadowRadius,
|
|
65
69
|
]);
|
|
66
70
|
useFrame(() => {
|
|
67
71
|
const csm = csmRef.current;
|
|
@@ -19,5 +19,5 @@ export function EntityPreview({ children, model, instanceId, mood = "studio", ba
|
|
|
19
19
|
const ctx = useOptionalGameContext();
|
|
20
20
|
const subject = children ??
|
|
21
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:
|
|
22
|
+
return (_jsx("div", { "data-jg-entity-preview": "", className: className, style: { position: "relative", width: "100%", height: "100%", ...style }, children: _jsx(Canvas, { shadows: "percentage", 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
23
|
}
|
|
@@ -1,29 +1,48 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
1
2
|
import type { EnvironmentSource } from "@jgengine/core/render/environment";
|
|
2
3
|
/** @internal Props for shell {@link EnvironmentLighting}. */
|
|
3
4
|
export interface EnvironmentLightingProps {
|
|
4
5
|
/** Optional authored environment map; absent or `gradient` uses the procedural probe. */
|
|
5
6
|
source?: EnvironmentSource;
|
|
6
|
-
/** IBL intensity. Default
|
|
7
|
+
/** IBL intensity. Default {@link DEFAULT_IBL_INTENSITY}. */
|
|
7
8
|
intensity?: number;
|
|
8
9
|
/** Upper-hemisphere tint for the procedural sky probe. Default cool daylight. */
|
|
9
10
|
skyColor?: string;
|
|
10
11
|
/** Ground / lower-hemisphere tint. Default soft earth. */
|
|
11
12
|
groundColor?: string;
|
|
13
|
+
/** Tint the horizon band leans toward, usually the scene's ambient colour so IBL keeps its warmth. Default white. */
|
|
14
|
+
horizonTint?: string;
|
|
12
15
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
16
|
+
* Sun direction (world space, not normalized) baked into the probe as an HDR disc plus a broad
|
|
17
|
+
* lobe, so metals glint and diffuse IBL has a lit side. Default {@link DEFAULT_PROBE_SUN}.
|
|
15
18
|
*/
|
|
16
19
|
sunDirection?: readonly [number, number, number];
|
|
17
20
|
/** Sun disc tint. Default warm white. */
|
|
18
21
|
sunColor?: string;
|
|
19
22
|
}
|
|
20
23
|
/**
|
|
21
|
-
* Image-based lighting from a procedural sky/ground probe (not three's stock
|
|
22
|
-
* RoomEnvironment gray studio box). Metals and dielectrics pick up sky color and
|
|
23
|
-
*
|
|
24
|
+
* Image-based lighting from a procedural sky/horizon/ground probe with an HDR sun (not three's stock
|
|
25
|
+
* RoomEnvironment gray studio box). Metals and dielectrics pick up sky color, a sun glint and a
|
|
26
|
+
* directional sky fill; re-render when sky/sun props change so a daylight cycle
|
|
24
27
|
* regenerates the environment map with the authored world.
|
|
25
28
|
*
|
|
26
29
|
* Opt out of cinematic IBL with `look: "flat"` on the shell presentation.
|
|
27
30
|
* @internal shell-internal default lighting; games never import it.
|
|
28
31
|
*/
|
|
29
|
-
export declare function EnvironmentLighting({ source, intensity, skyColor, groundColor, sunDirection, sunColor, }: EnvironmentLightingProps): null;
|
|
32
|
+
export declare function EnvironmentLighting({ source, intensity, skyColor, groundColor, horizonTint, sunDirection, sunColor, }: EnvironmentLightingProps): null;
|
|
33
|
+
/** @internal Default IBL strength for the procedural probe: enough sky fill and sun lobe to model forms. */
|
|
34
|
+
export declare const DEFAULT_IBL_INTENSITY = 0.45;
|
|
35
|
+
/** @internal Fallback probe sun when no directional light is authored: high and off to one side. */
|
|
36
|
+
export declare const DEFAULT_PROBE_SUN: readonly [number, number, number];
|
|
37
|
+
/**
|
|
38
|
+
* @internal Procedural sky probe: zenith → brighter horizon → ground bounce, plus an HDR sun disc and
|
|
39
|
+
* a wide glow lobe. After PMREM the lobe becomes a directional diffuse term, so the IBL alone gives
|
|
40
|
+
* forms a lit and a shaded side instead of a uniform two-tone wash.
|
|
41
|
+
*/
|
|
42
|
+
export declare function buildSkyProbeScene(options: {
|
|
43
|
+
skyColor: string;
|
|
44
|
+
groundColor: string;
|
|
45
|
+
horizonTint?: string;
|
|
46
|
+
sunDirection?: readonly [number, number, number];
|
|
47
|
+
sunColor: string;
|
|
48
|
+
}): THREE.Scene;
|
|
@@ -5,15 +5,15 @@ import * as THREE from "three";
|
|
|
5
5
|
import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js";
|
|
6
6
|
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";
|
|
7
7
|
/**
|
|
8
|
-
* Image-based lighting from a procedural sky/ground probe (not three's stock
|
|
9
|
-
* RoomEnvironment gray studio box). Metals and dielectrics pick up sky color and
|
|
10
|
-
*
|
|
8
|
+
* Image-based lighting from a procedural sky/horizon/ground probe with an HDR sun (not three's stock
|
|
9
|
+
* RoomEnvironment gray studio box). Metals and dielectrics pick up sky color, a sun glint and a
|
|
10
|
+
* directional sky fill; re-render when sky/sun props change so a daylight cycle
|
|
11
11
|
* regenerates the environment map with the authored world.
|
|
12
12
|
*
|
|
13
13
|
* Opt out of cinematic IBL with `look: "flat"` on the shell presentation.
|
|
14
14
|
* @internal shell-internal default lighting; games never import it.
|
|
15
15
|
*/
|
|
16
|
-
export function EnvironmentLighting({ source, intensity =
|
|
16
|
+
export function EnvironmentLighting({ source, intensity = DEFAULT_IBL_INTENSITY, skyColor = "#87b5e0", groundColor = "#3d4a38", horizonTint = "#ffffff", sunDirection, sunColor = "#fff2d6", }) {
|
|
17
17
|
const gl = useThree((state) => state.gl);
|
|
18
18
|
const scene = useThree((state) => state.scene);
|
|
19
19
|
useEffect(() => {
|
|
@@ -91,7 +91,7 @@ export function EnvironmentLighting({ source, intensity = 0.35, skyColor = "#87b
|
|
|
91
91
|
});
|
|
92
92
|
}
|
|
93
93
|
else {
|
|
94
|
-
const probe = buildSkyProbeScene({ skyColor: effective?.sky ?? skyColor, groundColor: effective?.ground ?? groundColor, sunDirection, sunColor: effective?.sun ?? sunColor });
|
|
94
|
+
const probe = buildSkyProbeScene({ skyColor: effective?.sky ?? skyColor, groundColor: effective?.ground ?? groundColor, horizonTint, sunDirection, sunColor: effective?.sun ?? sunColor });
|
|
95
95
|
try {
|
|
96
96
|
apply(pmrem.fromScene(probe, 0.04));
|
|
97
97
|
}
|
|
@@ -105,19 +105,36 @@ export function EnvironmentLighting({ source, intensity = 0.35, skyColor = "#87b
|
|
|
105
105
|
throw error;
|
|
106
106
|
}
|
|
107
107
|
return release;
|
|
108
|
-
}, [gl, scene, source, intensity, skyColor, groundColor, sunColor, sunDirection?.[0], sunDirection?.[1], sunDirection?.[2]]);
|
|
108
|
+
}, [gl, scene, source, intensity, skyColor, groundColor, horizonTint, sunColor, sunDirection?.[0], sunDirection?.[1], sunDirection?.[2]]);
|
|
109
109
|
return null;
|
|
110
110
|
}
|
|
111
|
-
|
|
111
|
+
/** @internal Default IBL strength for the procedural probe: enough sky fill and sun lobe to model forms. */
|
|
112
|
+
export const DEFAULT_IBL_INTENSITY = 0.45;
|
|
113
|
+
/** @internal Fallback probe sun when no directional light is authored: high and off to one side. */
|
|
114
|
+
export const DEFAULT_PROBE_SUN = [-0.45, 0.8, 0.35];
|
|
115
|
+
/**
|
|
116
|
+
* @internal Procedural sky probe: zenith → brighter horizon → ground bounce, plus an HDR sun disc and
|
|
117
|
+
* a wide glow lobe. After PMREM the lobe becomes a directional diffuse term, so the IBL alone gives
|
|
118
|
+
* forms a lit and a shaded side instead of a uniform two-tone wash.
|
|
119
|
+
*/
|
|
120
|
+
export function buildSkyProbeScene(options) {
|
|
112
121
|
const probe = new THREE.Scene();
|
|
113
|
-
|
|
114
|
-
|
|
122
|
+
const sun = new THREE.Vector3(...(options.sunDirection ?? DEFAULT_PROBE_SUN));
|
|
123
|
+
if (sun.lengthSq() < 1e-8)
|
|
124
|
+
sun.set(...DEFAULT_PROBE_SUN);
|
|
125
|
+
sun.normalize();
|
|
126
|
+
const sky = new THREE.Color(options.skyColor);
|
|
127
|
+
const horizon = sky.clone().lerp(new THREE.Color(options.horizonTint ?? "#ffffff"), 0.35);
|
|
128
|
+
const geo = new THREE.SphereGeometry(50, 48, 24);
|
|
115
129
|
const mat = new THREE.ShaderMaterial({
|
|
116
130
|
side: THREE.BackSide,
|
|
117
131
|
depthWrite: false,
|
|
118
132
|
uniforms: {
|
|
119
|
-
|
|
133
|
+
zenith: { value: sky.clone().multiplyScalar(0.85) },
|
|
134
|
+
horizon: { value: horizon },
|
|
120
135
|
ground: { value: new THREE.Color(options.groundColor) },
|
|
136
|
+
sunDir: { value: sun },
|
|
137
|
+
sunColor: { value: new THREE.Color(options.sunColor) },
|
|
121
138
|
},
|
|
122
139
|
vertexShader: /* glsl */ `
|
|
123
140
|
varying vec3 vDir;
|
|
@@ -127,30 +144,26 @@ function buildSkyProbeScene(options) {
|
|
|
127
144
|
}
|
|
128
145
|
`,
|
|
129
146
|
fragmentShader: /* glsl */ `
|
|
130
|
-
uniform vec3
|
|
147
|
+
uniform vec3 zenith;
|
|
148
|
+
uniform vec3 horizon;
|
|
131
149
|
uniform vec3 ground;
|
|
150
|
+
uniform vec3 sunDir;
|
|
151
|
+
uniform vec3 sunColor;
|
|
132
152
|
varying vec3 vDir;
|
|
133
153
|
void main() {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
154
|
+
vec3 d = normalize(vDir);
|
|
155
|
+
float up = clamp(d.y, -1.0, 1.0);
|
|
156
|
+
vec3 col = up >= 0.0
|
|
157
|
+
? mix(horizon, zenith, pow(up, 0.6))
|
|
158
|
+
: mix(horizon * 0.55 + ground * 0.45, ground * 0.7, smoothstep(0.0, 0.5, -up));
|
|
159
|
+
float facing = max(dot(d, sunDir), 0.0);
|
|
160
|
+
// Disc for specular glints; the broad lobe is what survives PMREM into the diffuse term.
|
|
161
|
+
col += sunColor * (pow(facing, 900.0) * 24.0 + pow(facing, 6.0) * 0.6 + pow(facing, 2.0) * 0.1);
|
|
162
|
+
gl_FragColor = vec4(col, 1.0);
|
|
138
163
|
}
|
|
139
164
|
`,
|
|
140
165
|
});
|
|
141
166
|
probe.add(new THREE.Mesh(geo, mat));
|
|
142
|
-
if (options.sunDirection !== undefined) {
|
|
143
|
-
const dir = new THREE.Vector3(options.sunDirection[0], options.sunDirection[1], options.sunDirection[2]);
|
|
144
|
-
if (dir.lengthSq() > 1e-8) {
|
|
145
|
-
dir.normalize();
|
|
146
|
-
const sunGeo = new THREE.SphereGeometry(3.5, 16, 12);
|
|
147
|
-
const sunMat = new THREE.MeshBasicMaterial({ color: options.sunColor });
|
|
148
|
-
const sun = new THREE.Mesh(sunGeo, sunMat);
|
|
149
|
-
// Place the disc outside the sky sphere sample so it stays a compact highlight.
|
|
150
|
-
sun.position.copy(dir.multiplyScalar(42));
|
|
151
|
-
probe.add(sun);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
167
|
return probe;
|
|
155
168
|
}
|
|
156
169
|
function disposeScene(scene) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useLayoutEffect, useRef } from "react";
|
|
3
|
-
import { CascadedShadows } from "./CascadedShadows.js";
|
|
3
|
+
import { CascadedShadows, DEFAULT_SHADOW_RADIUS } from "./CascadedShadows.js";
|
|
4
4
|
const DEFAULT_BACKDROP_FOG_COLOR = "#1a1c22";
|
|
5
5
|
/** Default cap for combined dynamic point and spot lights. */
|
|
6
6
|
export const POINT_LIGHT_BUDGET = 8;
|
|
@@ -58,7 +58,7 @@ function SingleShadowLight({ entry }) {
|
|
|
58
58
|
light.shadow.map.dispose();
|
|
59
59
|
light.shadow.map = null;
|
|
60
60
|
}, [mapSize]);
|
|
61
|
-
return (_jsx("directionalLight", { ref: lightRef, 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": mapSize, "shadow-mapSize-height": mapSize, "shadow-camera-left": -size, "shadow-camera-right": size, "shadow-camera-top": size, "shadow-camera-bottom": -size, "shadow-camera-near": 0.5, "shadow-camera-far": far, "shadow-bias": entry.shadowBias ?? -0.0004, "shadow-normalBias": entry.shadowNormalBias ?? 0.02 }));
|
|
61
|
+
return (_jsx("directionalLight", { ref: lightRef, 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": mapSize, "shadow-mapSize-height": mapSize, "shadow-camera-left": -size, "shadow-camera-right": size, "shadow-camera-top": size, "shadow-camera-bottom": -size, "shadow-camera-near": 0.5, "shadow-camera-far": far, "shadow-bias": entry.shadowBias ?? -0.0004, "shadow-normalBias": entry.shadowNormalBias ?? 0.02, "shadow-radius": entry.shadowRadius ?? DEFAULT_SHADOW_RADIUS }));
|
|
62
62
|
}
|
|
63
63
|
export function ConfiguredLighting({ lighting, profile }) {
|
|
64
64
|
const dynamic = resolveDynamicLights(lighting);
|
|
@@ -122,5 +122,12 @@ export declare function materialKtx2Loader(): KTX2Loader;
|
|
|
122
122
|
* @capability model-load-idle how long the shared GLB loader has been idle, for capture settle waits
|
|
123
123
|
*/
|
|
124
124
|
export declare function modelLoadIdleMs(): number;
|
|
125
|
+
/**
|
|
126
|
+
* How many times the shared GLB loader has gone from idle to busy since boot. A capture host
|
|
127
|
+
* compares two readings around a rendered frame to tell whether that frame started new model
|
|
128
|
+
* streaming, which an idle reading alone cannot show.
|
|
129
|
+
* @capability model-load-starts count of model streaming bursts, for capture quiescence checks
|
|
130
|
+
*/
|
|
131
|
+
export declare function modelLoadStarts(): number;
|
|
125
132
|
/** Shared GLTF loader used by shell entity/object model mounts. @internal */
|
|
126
133
|
export declare const sharedGltfLoader: DiagnosticGLTFLoader;
|
package/dist/render/modelLoad.js
CHANGED
|
@@ -6,6 +6,7 @@ import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.j
|
|
|
6
6
|
import { classifyAssetResponse, } from "@jgengine/core/scene/assetDiagnostics";
|
|
7
7
|
import { reportTextureLoadError } from "@jgengine/core/devtools/textureErrors";
|
|
8
8
|
import { resolveAssetBaseUrl } from "./assetBase.js";
|
|
9
|
+
import { applyModelSurface } from "./modelSurface.js";
|
|
9
10
|
/** Model containers already covered by the whole-model fallback probe; not "texture" errors. @internal */
|
|
10
11
|
const MODEL_URL_RE = /\.(glb|gltf)(\?|#|$)/i;
|
|
11
12
|
/**
|
|
@@ -210,6 +211,7 @@ export class DiagnosticGLTFLoader extends GLTFLoader {
|
|
|
210
211
|
load(url, onLoad, onProgress, onError) {
|
|
211
212
|
super.load(url, (gltf) => {
|
|
212
213
|
recordModelTriangles(url, countSceneTriangles(gltf.scene));
|
|
214
|
+
applyModelSurface(gltf.scene);
|
|
213
215
|
onLoad(gltf);
|
|
214
216
|
}, onProgress, (event) => {
|
|
215
217
|
if (onError === undefined)
|
|
@@ -258,9 +260,11 @@ export function materialKtx2Loader() {
|
|
|
258
260
|
// a drain timestamp is the whole state.
|
|
259
261
|
let modelQueueBusy = false;
|
|
260
262
|
let modelQueueDrainedAt = 0;
|
|
263
|
+
let modelQueueStarts = 0;
|
|
261
264
|
const now = () => (typeof performance === "undefined" ? Date.now() : performance.now());
|
|
262
265
|
modelLoadingManager.onStart = () => {
|
|
263
266
|
modelQueueBusy = true;
|
|
267
|
+
modelQueueStarts += 1;
|
|
264
268
|
};
|
|
265
269
|
modelLoadingManager.onLoad = () => {
|
|
266
270
|
modelQueueBusy = false;
|
|
@@ -280,6 +284,15 @@ export function modelLoadIdleMs() {
|
|
|
280
284
|
return Number.POSITIVE_INFINITY;
|
|
281
285
|
return now() - modelQueueDrainedAt;
|
|
282
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* How many times the shared GLB loader has gone from idle to busy since boot. A capture host
|
|
289
|
+
* compares two readings around a rendered frame to tell whether that frame started new model
|
|
290
|
+
* streaming, which an idle reading alone cannot show.
|
|
291
|
+
* @capability model-load-starts count of model streaming bursts, for capture quiescence checks
|
|
292
|
+
*/
|
|
293
|
+
export function modelLoadStarts() {
|
|
294
|
+
return modelQueueStarts;
|
|
295
|
+
}
|
|
283
296
|
/** Shared GLTF loader used by shell entity/object model mounts. @internal */
|
|
284
297
|
export const sharedGltfLoader = new DiagnosticGLTFLoader(modelLoadingManager);
|
|
285
298
|
sharedGltfLoader.setMeshoptDecoder(MeshoptDecoder);
|
|
@@ -19,6 +19,7 @@ export declare function modelPlacementTransform(root: THREE.Object3D, model: Mod
|
|
|
19
19
|
export declare function cloneModelScene(source: THREE.Object3D, options?: {
|
|
20
20
|
cloneMaterials?: boolean;
|
|
21
21
|
shadows?: ModelShadowMode;
|
|
22
|
+
surfaceDetail?: boolean;
|
|
22
23
|
}): THREE.Object3D;
|
|
23
24
|
/**
|
|
24
25
|
* Release materials and bone textures owned by `cloneModelScene`; shared assets and attached models remain owned by their callers.
|
|
@@ -2,6 +2,7 @@ import * as THREE from "three";
|
|
|
2
2
|
import { clone as cloneSkinned } from "three/examples/jsm/utils/SkeletonUtils.js";
|
|
3
3
|
import { measureLocalBounds } from "./measureBounds.js";
|
|
4
4
|
import { copyModelBindPose } from "./modelBindPose.js";
|
|
5
|
+
import { optOutModelSurface } from "./modelSurface.js";
|
|
5
6
|
export const PAINT_TEXTURE_SIZE = 512;
|
|
6
7
|
const modelResources = new WeakMap();
|
|
7
8
|
/** Register an instance replacement without transferring borrowed geometry or textures. @internal */
|
|
@@ -56,6 +57,8 @@ export function cloneModelScene(source, options) {
|
|
|
56
57
|
material = source.clone();
|
|
57
58
|
material.onBeforeCompile = source.onBeforeCompile;
|
|
58
59
|
material.customProgramCacheKey = source.customProgramCacheKey;
|
|
60
|
+
if (options?.surfaceDetail === false)
|
|
61
|
+
optOutModelSurface(material);
|
|
59
62
|
materials.set(source, material);
|
|
60
63
|
}
|
|
61
64
|
return material;
|