@jgengine/shell 0.18.1 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +504 -7
- package/dist/GameHost.d.ts +12 -1
- package/dist/GameHost.js +14 -1
- package/dist/GamePlayerShell.js +132 -17
- package/dist/Shell3dPresentation.d.ts +5 -11
- package/dist/Shell3dPresentation.js +73 -11
- package/dist/ShellChrome.d.ts +2 -1
- package/dist/ShellChrome.js +22 -6
- package/dist/ShellHudPresentation.d.ts +20 -2
- package/dist/ShellHudPresentation.js +41 -6
- package/dist/audio/AudioComponents.js +6 -2
- package/dist/audio/audioEngine.d.ts +24 -0
- package/dist/audio/audioEngine.js +231 -83
- package/dist/audio/audioWire.d.ts +3 -0
- package/dist/audio/audioWire.js +9 -3
- package/dist/audio/loopParams.d.ts +39 -0
- package/dist/audio/loopParams.js +43 -0
- package/dist/audio/synthEngine.d.ts +7 -1
- package/dist/audio/synthEngine.js +103 -38
- package/dist/camera/GameCameraRig.js +3 -2
- package/dist/camera/GameFirstPersonCamera.d.ts +6 -4
- package/dist/camera/GameFirstPersonCamera.js +68 -27
- package/dist/camera/Viewports.d.ts +34 -0
- package/dist/camera/Viewports.js +123 -0
- package/dist/camera/cameraRigs.js +76 -62
- package/dist/camera/chaseHeading.d.ts +9 -0
- package/dist/camera/chaseHeading.js +10 -0
- package/dist/camera/customMuzzle.d.ts +14 -0
- package/dist/camera/customMuzzle.js +68 -0
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/orbitCameraMath.js +2 -2
- package/dist/camera/rigMath.d.ts +82 -4
- package/dist/camera/rigMath.js +133 -15
- package/dist/camera/rigResolve.d.ts +3 -0
- package/dist/camera/rigResolve.js +11 -0
- package/dist/combat/ProjectileModels.d.ts +31 -0
- package/dist/combat/ProjectileModels.js +70 -0
- package/dist/commandSink.d.ts +1 -2
- package/dist/commandSink.js +3 -2
- package/dist/defineGame.d.ts +19 -3
- package/dist/defineGame.js +39 -7
- package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
- package/dist/devtools/DevtoolsOverlay.js +15 -167
- package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
- package/dist/devtools/DevtoolsRuntime.js +61 -0
- package/dist/devtools/agentBridge.d.ts +6 -0
- package/dist/devtools/agentBridge.js +16 -2
- package/dist/devtools/devtoolsReports.d.ts +68 -0
- package/dist/devtools/devtoolsReports.js +100 -0
- package/dist/devtools/latencyInstrumentation.d.ts +3 -0
- package/dist/devtools/latencyInstrumentation.js +11 -0
- package/dist/devtools/playtestProbe.d.ts +4 -0
- package/dist/devtools/playtestProbe.js +22 -0
- package/dist/diagnostics/FpsCounter.d.ts +17 -0
- package/dist/diagnostics/FpsCounter.js +32 -0
- package/dist/drivers/FrameDriver.js +35 -20
- package/dist/drivers/FrameRateLimiter.d.ts +9 -0
- package/dist/drivers/FrameRateLimiter.js +39 -0
- package/dist/drivers/HudOnlyDriver.d.ts +6 -1
- package/dist/drivers/HudOnlyDriver.js +32 -14
- package/dist/environment/DayNightSky.js +5 -5
- package/dist/environment/Daylight.d.ts +35 -1
- package/dist/environment/Daylight.js +98 -28
- package/dist/environment/EnvironmentScene.js +11 -10
- package/dist/environment/VolumetricClouds.js +20 -2
- package/dist/environment/daylightCycle.d.ts +20 -0
- package/dist/environment/daylightCycle.js +59 -7
- package/dist/environment/sunShadowMath.d.ts +45 -0
- package/dist/environment/sunShadowMath.js +50 -0
- package/dist/input/gamepadPoll.d.ts +47 -0
- package/dist/input/gamepadPoll.js +142 -0
- package/dist/input/gamepadSource.d.ts +11 -1
- package/dist/input/gamepadSource.js +117 -71
- package/dist/input/mouseLook.js +2 -1
- package/dist/input/padHaptics.d.ts +17 -0
- package/dist/input/padHaptics.js +32 -0
- package/dist/input/pointerLock.d.ts +11 -0
- package/dist/input/pointerLock.js +19 -0
- package/dist/inputSink.d.ts +6 -6
- package/dist/inputSink.js +60 -21
- package/dist/lazyShell3dPresentation.d.ts +9 -0
- package/dist/lazyShell3dPresentation.js +24 -0
- package/dist/materialOverride.d.ts +42 -0
- package/dist/materialOverride.js +95 -36
- package/dist/multiplayer.d.ts +33 -3
- package/dist/multiplayer.js +105 -22
- package/dist/pointer/pointerService.d.ts +2 -0
- package/dist/pointer/pointerService.js +12 -2
- package/dist/postfx/PostProcessing.d.ts +11 -1
- package/dist/postfx/PostProcessing.js +14 -42
- package/dist/postfx/gradeShader.d.ts +1 -1
- package/dist/postfx/gradeShader.js +30 -2
- package/dist/postfx/shadowPrepass.d.ts +3 -0
- package/dist/postfx/shadowPrepass.js +18 -0
- package/dist/postfx/stylizePass.d.ts +23 -0
- package/dist/postfx/stylizePass.js +216 -0
- package/dist/presentationRecovery.d.ts +75 -0
- package/dist/presentationRecovery.js +204 -0
- package/dist/presentationRecovery3d.d.ts +4 -0
- package/dist/presentationRecovery3d.js +32 -0
- package/dist/presentationResolve.d.ts +8 -1
- package/dist/presentationResolve.js +5 -2
- package/dist/render/CascadedShadows.d.ts +7 -2
- package/dist/render/CascadedShadows.js +98 -20
- package/dist/render/EnvironmentLighting.js +74 -35
- package/dist/render/HairCards.d.ts +7 -0
- package/dist/render/HairCards.js +23 -0
- package/dist/render/SceneModels.js +52 -78
- package/dist/render/SkinnedInstances.d.ts +91 -0
- package/dist/render/SkinnedInstances.js +249 -0
- package/dist/render/TextureFiltering.d.ts +16 -0
- package/dist/render/TextureFiltering.js +64 -0
- package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
- package/dist/render/authoredSurfaceMaterial.js +108 -0
- package/dist/render/displayColor.d.ts +33 -0
- package/dist/render/displayColor.js +138 -0
- package/dist/render/materialAppearanceSignals.d.ts +23 -0
- package/dist/render/materialAppearanceSignals.js +58 -0
- package/dist/render/materialAsset.d.ts +98 -0
- package/dist/render/materialAsset.js +454 -0
- package/dist/render/measureBounds.js +16 -3
- package/dist/render/measureCollisionMesh.js +8 -1
- package/dist/render/modelAssets.d.ts +8 -0
- package/dist/render/modelAssets.js +43 -0
- package/dist/render/modelBindPose.d.ts +5 -0
- package/dist/render/modelBindPose.js +35 -0
- package/dist/render/modelLoad.d.ts +32 -0
- package/dist/render/modelLoad.js +70 -1
- package/dist/render/modelNodeVisibility.d.ts +6 -0
- package/dist/render/modelNodeVisibility.js +18 -0
- package/dist/render/modelRender.d.ts +18 -1
- package/dist/render/modelRender.js +81 -3
- package/dist/render/projectedOverdraw.d.ts +35 -0
- package/dist/render/projectedOverdraw.js +204 -0
- package/dist/render/resolveModel.d.ts +12 -2
- package/dist/render/resolveModel.js +25 -4
- package/dist/render/rigNode.d.ts +26 -0
- package/dist/render/rigNode.js +32 -0
- package/dist/render/spriteRender.d.ts +4 -0
- package/dist/render/spriteRender.js +5 -0
- package/dist/render/staticShapeInstances.d.ts +34 -0
- package/dist/render/staticShapeInstances.js +78 -0
- package/dist/render/useFootIk.d.ts +53 -4
- package/dist/render/useFootIk.js +262 -70
- package/dist/render/useModelAnimation.d.ts +50 -0
- package/dist/render/useModelAnimation.js +245 -99
- package/dist/render/useModelInstance.d.ts +25 -0
- package/dist/render/useModelInstance.js +43 -0
- package/dist/scatter/scatterModels.d.ts +3 -2
- package/dist/scatter/scatterModels.js +15 -7
- package/dist/scene/AuthoredScene.d.ts +23 -6
- package/dist/scene/AuthoredScene.js +46 -9
- package/dist/scene/CityRenderer.js +6 -6
- package/dist/scene/GeneratedAssetRenderer.js +11 -42
- package/dist/settings/QuickControls.js +1 -1
- package/dist/settings/SettingsMenu.js +11 -18
- package/dist/settings/SettingsRuntime.d.ts +5 -0
- package/dist/settings/SettingsRuntime.js +29 -2
- package/dist/settings/appliedSettings.d.ts +9 -3
- package/dist/settings/appliedSettings.js +6 -5
- package/dist/settings/settingsController.d.ts +3 -0
- package/dist/settings/settingsController.js +47 -3
- package/dist/shellControlSuspension.d.ts +18 -0
- package/dist/shellControlSuspension.js +28 -0
- package/dist/shellInputPublication.d.ts +28 -0
- package/dist/shellInputPublication.js +49 -0
- package/dist/shellPointerInput.d.ts +18 -0
- package/dist/shellPointerInput.js +49 -0
- package/dist/structures/BuildingKitBatch.d.ts +3 -1
- package/dist/structures/BuildingKitBatch.js +35 -56
- package/dist/structures/BuildingRenderBudget.d.ts +23 -0
- package/dist/structures/BuildingRenderBudget.js +54 -0
- package/dist/structures/GeneratedBuilding.d.ts +3 -15
- package/dist/structures/GeneratedBuilding.js +33 -22
- package/dist/structures/buildingKitMaterials.d.ts +20 -0
- package/dist/structures/buildingKitMaterials.js +70 -0
- package/dist/structures/buildingSpatialBatch.d.ts +11 -0
- package/dist/structures/buildingSpatialBatch.js +45 -0
- package/dist/structures/buildingSurface.d.ts +21 -0
- package/dist/structures/buildingSurface.js +134 -0
- package/dist/terrain/GrassField.d.ts +2 -1
- package/dist/terrain/GrassField.js +59 -5
- package/dist/terrain/grassGeometry.d.ts +10 -0
- package/dist/terrain/grassGeometry.js +55 -0
- package/dist/terrain/grassMaterial.js +2 -2
- package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
- package/dist/terrain/terrainDetailMaterial.js +7 -7
- package/dist/touch/TouchControlsOverlay.js +4 -4
- package/dist/useShellMultiplayerSync.d.ts +1 -1
- package/dist/useShellMultiplayerSync.js +9 -3
- package/dist/vfx/ParticleField.d.ts +23 -20
- package/dist/vfx/ParticleField.js +168 -50
- package/dist/visibility/CullingProvider.js +2 -62
- package/dist/visibility/cullingDriver.d.ts +9 -0
- package/dist/visibility/cullingDriver.js +68 -0
- package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
- package/dist/weather/AuthoredWeatherLayer.js +31 -0
- package/dist/weather/DustField.d.ts +2 -1
- package/dist/weather/DustField.js +17 -3
- package/dist/weather/FireSpreadLayer.d.ts +4 -2
- package/dist/weather/FireSpreadLayer.js +5 -11
- package/dist/weather/LightningStrike.d.ts +4 -1
- package/dist/weather/LightningStrike.js +13 -17
- package/dist/weather/RainField.d.ts +2 -1
- package/dist/weather/RainField.js +17 -3
- package/dist/weather/RainImpactField.d.ts +16 -0
- package/dist/weather/RainImpactField.js +89 -0
- package/dist/weather/SnowField.d.ts +2 -1
- package/dist/weather/SnowField.js +17 -3
- package/dist/weather/WeatherLayer.d.ts +11 -2
- package/dist/weather/WeatherLayer.js +30 -6
- package/dist/weather/index.d.ts +2 -0
- package/dist/weather/index.js +2 -0
- package/dist/weather/weatherMath.d.ts +2 -0
- package/dist/weather/weatherMath.js +8 -0
- package/dist/weather/weatherShelter.d.ts +18 -0
- package/dist/weather/weatherShelter.js +75 -0
- package/dist/weather/weatherUniforms.d.ts +40 -1
- package/dist/weather/weatherUniforms.js +29 -9
- package/dist/world/AuthoredFlocks.d.ts +22 -0
- package/dist/world/AuthoredFlocks.js +87 -0
- package/dist/world/StaticObjectModels.d.ts +8 -0
- package/dist/world/StaticObjectModels.js +75 -0
- package/dist/world/WorldHud.d.ts +14 -6
- package/dist/world/WorldHud.js +25 -12
- package/dist/world/WorldParticles.d.ts +10 -23
- package/dist/world/WorldParticles.js +210 -62
- package/dist/world/WorldScene.d.ts +3 -1
- package/dist/world/WorldScene.js +41 -26
- package/dist/world/staticObjectBatches.d.ts +35 -0
- package/dist/world/staticObjectBatches.js +163 -0
- package/dist/world/worldBarSamples.d.ts +37 -33
- package/dist/world/worldBarSamples.js +117 -75
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +2 -2
- package/skills/game-design/SKILL.md +2 -2
- package/skills/jgengine/SKILL.md +28 -6
- package/skills/jgengine/api.md +139 -99
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine/recipes/minimal-game.md +10 -2
- package/skills/jgengine-ui/SKILL.md +16 -17
- package/skills/jgengine-ui/api.md +603 -365
- package/skills/jgengine-ui/capabilities.md +131 -1
- package/skills/jgengine-ui/reference-primitives.md +179 -0
- package/skills/jgengine-ui/reference.md +4 -81
- package/skills/jgengine-world/SKILL.md +25 -4
- package/skills/jgengine-world/api.md +1045 -655
- package/skills/jgengine-world/capabilities.md +246 -9
- package/skills/jgengine-world/recipes/character-animation.md +62 -0
- package/skills/jgengine-world/recipes/controls.md +48 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +55 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
- package/skills/jgengine-world/reference.md +180 -4
- package/skills/jgengine-world/visibility.md +139 -0
- package/skills/level-design/SKILL.md +1 -1
- package/dist/render/testFixtures/proceduralSpriteAtlas.d.ts +0 -15
- package/dist/render/testFixtures/proceduralSpriteAtlas.js +0 -30
- package/dist/render/testFixtures/proceduralTileLayer.d.ts +0 -13
- package/dist/render/testFixtures/proceduralTileLayer.js +0 -30
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
import { GLTFLoader, type GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
3
|
+
import { KTX2Loader } from "three/examples/jsm/loaders/KTX2Loader.js";
|
|
3
4
|
import { type AssetLoadDiagnosis } from "@jgengine/core/scene/assetDiagnostics";
|
|
4
5
|
/**
|
|
5
6
|
* The shared GLB {@link THREE.LoadingManager}'s `onError` sink. It fires for EVERY item the manager
|
|
@@ -61,6 +62,35 @@ export declare function modelLoadFallbacks(): readonly {
|
|
|
61
62
|
}[];
|
|
62
63
|
/** Drops the recorded placeholder list. For tests and between capture runs. @internal */
|
|
63
64
|
export declare function clearModelLoadFallbacks(): void;
|
|
65
|
+
/** Per-model triangle ceiling before the loader warns. A prop is a few thousand triangles; a hero rig tens of thousands. */
|
|
66
|
+
export declare const DEFAULT_MODEL_TRIANGLE_BUDGET = 100000;
|
|
67
|
+
/** 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. */
|
|
68
|
+
export declare function setModelTriangleBudget(triangles: number): void;
|
|
69
|
+
/** Triangles a loaded model contributes per draw, summing every mesh (instanced meshes count once per instance). @internal */
|
|
70
|
+
export declare function countSceneTriangles(root: THREE.Object3D): number;
|
|
71
|
+
/** One over-budget model as {@link heavyModels} reports it: the URL the loader fetched, its triangle count, and the budget it exceeded. */
|
|
72
|
+
export interface HeavyModelRecord {
|
|
73
|
+
url: string;
|
|
74
|
+
triangles: number;
|
|
75
|
+
budget: number;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Record a loaded model's triangle count against the budget: over budget warns once per URL and lands in
|
|
79
|
+
* {@link heavyModels}; under budget is silent. Split out so the wiring is unit-testable without a GL context.
|
|
80
|
+
* @internal
|
|
81
|
+
*/
|
|
82
|
+
export declare function recordModelTriangles(url: string, triangles: number, overrides?: {
|
|
83
|
+
warn?: (record: HeavyModelRecord) => void;
|
|
84
|
+
}): void;
|
|
85
|
+
/**
|
|
86
|
+
* Every model the shared loader resolved whose triangle count exceeds the budget, in load order. A heavy
|
|
87
|
+
* prop is invisible in a screenshot and only shows up as a slow frame, so the loader measures each GLB as it
|
|
88
|
+
* lands and this exposes the verdict for `debug_snapshot` probes and smoke tests to assert against.
|
|
89
|
+
* @capability heavy-models models over the per-model triangle budget, for perf probes and smoke assertions
|
|
90
|
+
*/
|
|
91
|
+
export declare function heavyModels(): readonly HeavyModelRecord[];
|
|
92
|
+
/** Drops the heavy-model list and the once-per-URL warning memory. For tests and between capture runs. @internal */
|
|
93
|
+
export declare function clearHeavyModels(): void;
|
|
64
94
|
/**
|
|
65
95
|
* A {@link GLTFLoader} whose success path is unchanged but whose failures degrade
|
|
66
96
|
* gracefully: when a load errors, it probes the URL and — for a diagnosed broken
|
|
@@ -82,6 +112,8 @@ export interface ModelLoaderConfig {
|
|
|
82
112
|
export declare function configureModelLoaders(options?: ModelLoaderConfig): Readonly<Required<ModelLoaderConfig>>;
|
|
83
113
|
/** Detects GPU support for the configured KTX2 transcoder after a renderer exists. */
|
|
84
114
|
export declare function detectKtx2Support(renderer: THREE.WebGLRenderer): void;
|
|
115
|
+
/** Shared transcoder for authored material maps; GPU support is detected by the canvas. @internal */
|
|
116
|
+
export declare function materialKtx2Loader(): KTX2Loader;
|
|
85
117
|
/**
|
|
86
118
|
* How long the shared GLB loader has been idle, in ms — `0` while any model is still
|
|
87
119
|
* in flight. A capture host reads this to wait for streaming to finish instead of
|
package/dist/render/modelLoad.js
CHANGED
|
@@ -140,6 +140,63 @@ export function modelLoadFallbacks() {
|
|
|
140
140
|
export function clearModelLoadFallbacks() {
|
|
141
141
|
servedFallbacks.length = 0;
|
|
142
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
|
+
}
|
|
143
200
|
/**
|
|
144
201
|
* A {@link GLTFLoader} whose success path is unchanged but whose failures degrade
|
|
145
202
|
* gracefully: when a load errors, it probes the URL and — for a diagnosed broken
|
|
@@ -151,7 +208,10 @@ export function clearModelLoadFallbacks() {
|
|
|
151
208
|
*/
|
|
152
209
|
export class DiagnosticGLTFLoader extends GLTFLoader {
|
|
153
210
|
load(url, onLoad, onProgress, onError) {
|
|
154
|
-
super.load(url,
|
|
211
|
+
super.load(url, (gltf) => {
|
|
212
|
+
recordModelTriangles(url, countSceneTriangles(gltf.scene));
|
|
213
|
+
onLoad(gltf);
|
|
214
|
+
}, onProgress, (event) => {
|
|
155
215
|
if (onError === undefined)
|
|
156
216
|
return;
|
|
157
217
|
// Probe the URL the loader actually fetched (base-resolved), not the raw input.
|
|
@@ -163,6 +223,7 @@ const DEFAULT_DRACO_DECODER_PATH = "https://cdn.jsdelivr.net/npm/three@0.182.0/e
|
|
|
163
223
|
const DEFAULT_KTX2_TRANSCODER_PATH = "https://cdn.jsdelivr.net/npm/three@0.182.0/examples/jsm/libs/basis/";
|
|
164
224
|
let configuredModelLoaders;
|
|
165
225
|
let ktx2Loader;
|
|
226
|
+
const ktx2DetectedRenderers = new WeakMap();
|
|
166
227
|
/** Configures shared Draco and KTX2 loaders. Repeated calls with the same paths are no-ops. */
|
|
167
228
|
export function configureModelLoaders(options = {}) {
|
|
168
229
|
const next = {
|
|
@@ -182,7 +243,15 @@ export function configureModelLoaders(options = {}) {
|
|
|
182
243
|
/** Detects GPU support for the configured KTX2 transcoder after a renderer exists. */
|
|
183
244
|
export function detectKtx2Support(renderer) {
|
|
184
245
|
configureModelLoaders();
|
|
246
|
+
if (ktx2DetectedRenderers.get(renderer) === ktx2Loader)
|
|
247
|
+
return;
|
|
185
248
|
ktx2Loader?.detectSupport(renderer);
|
|
249
|
+
ktx2DetectedRenderers.set(renderer, ktx2Loader);
|
|
250
|
+
}
|
|
251
|
+
/** Shared transcoder for authored material maps; GPU support is detected by the canvas. @internal */
|
|
252
|
+
export function materialKtx2Loader() {
|
|
253
|
+
configureModelLoaders();
|
|
254
|
+
return ktx2Loader;
|
|
186
255
|
}
|
|
187
256
|
// LoadingManager reports queue transitions, not per-item events: `onStart` fires when the
|
|
188
257
|
// queue goes busy and `onLoad` when it drains (errors included, via itemEnd), so a flag plus
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Object3D } from "three";
|
|
2
|
+
import { type RigNodeDiagnostic } from "./rigNode.js";
|
|
3
|
+
/** Order and duplicate references do not change a model instance's visibility selection. @internal */
|
|
4
|
+
export declare function modelHiddenNodesKey(names?: readonly string[]): string;
|
|
5
|
+
/** Apply visibility only to uniquely resolved nodes on an owned clone, retaining imported hidden state and rig hierarchy. @internal */
|
|
6
|
+
export declare function applyModelNodeVisibility(root: Object3D, names?: readonly string[]): RigNodeDiagnostic[];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { parseModelHiddenNodes } from "@jgengine/core/editor/modelNodeVisibility";
|
|
2
|
+
import { resolveRigNode } from "./rigNode.js";
|
|
3
|
+
/** Order and duplicate references do not change a model instance's visibility selection. @internal */
|
|
4
|
+
export function modelHiddenNodesKey(names = []) {
|
|
5
|
+
return JSON.stringify([...new Set(names)].sort());
|
|
6
|
+
}
|
|
7
|
+
/** Apply visibility only to uniquely resolved nodes on an owned clone, retaining imported hidden state and rig hierarchy. @internal */
|
|
8
|
+
export function applyModelNodeVisibility(root, names = []) {
|
|
9
|
+
const diagnostics = [];
|
|
10
|
+
for (const name of new Set(parseModelHiddenNodes(names))) {
|
|
11
|
+
const resolved = resolveRigNode(root, name);
|
|
12
|
+
if (resolved.node === undefined)
|
|
13
|
+
diagnostics.push(resolved.diagnostic);
|
|
14
|
+
else
|
|
15
|
+
resolved.node.visible = false;
|
|
16
|
+
}
|
|
17
|
+
return diagnostics;
|
|
18
|
+
}
|
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
import type { PaintStroke } from "@jgengine/core/scene/paintLayer";
|
|
3
|
+
import type { ModelConfig } from "@jgengine/core/game/playableGame";
|
|
4
|
+
import { type MeasuredLocalBounds } from "./measureBounds.js";
|
|
3
5
|
export declare const PAINT_TEXTURE_SIZE = 512;
|
|
4
6
|
/** Shadow participation applied to every mesh of a cloned model; mirrors `ModelConfig.shadows`. */
|
|
5
7
|
export type ModelShadowMode = "cast" | "receive" | "both" | "none";
|
|
6
|
-
/** @internal */
|
|
8
|
+
/** Register an instance replacement without transferring borrowed geometry or textures. @internal */
|
|
9
|
+
export declare function ownModelMaterial(root: THREE.Object3D, material: THREE.Material): boolean;
|
|
10
|
+
/** Resolve placement from bind bounds in the frame containing the imported root, shared by rendering and collider measurement. @internal */
|
|
11
|
+
export declare function modelPlacementTransform(root: THREE.Object3D, model: ModelConfig, bindBounds?: MeasuredLocalBounds | null): {
|
|
12
|
+
scale: number;
|
|
13
|
+
position: [number, number, number];
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Clone a model with independent pose and materials, retaining shared geometry and textures.
|
|
17
|
+
* @capability model-instance clone a loaded model for independent styling and animation
|
|
18
|
+
*/
|
|
7
19
|
export declare function cloneModelScene(source: THREE.Object3D, options?: {
|
|
8
20
|
cloneMaterials?: boolean;
|
|
9
21
|
shadows?: ModelShadowMode;
|
|
10
22
|
}): THREE.Object3D;
|
|
23
|
+
/**
|
|
24
|
+
* Release materials and bone textures owned by `cloneModelScene`; shared assets and attached models remain owned by their callers.
|
|
25
|
+
* @capability model-instance release a cloned model without disposing cached geometry or textures
|
|
26
|
+
*/
|
|
27
|
+
export declare function disposeModelScene(root: THREE.Object3D): void;
|
|
11
28
|
/** @internal */
|
|
12
29
|
export declare function disposeClonedMaterials(root: THREE.Object3D): void;
|
|
13
30
|
/** @internal */
|
|
@@ -1,25 +1,103 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
import { clone as cloneSkinned } from "three/examples/jsm/utils/SkeletonUtils.js";
|
|
3
|
+
import { measureLocalBounds } from "./measureBounds.js";
|
|
4
|
+
import { copyModelBindPose } from "./modelBindPose.js";
|
|
3
5
|
export const PAINT_TEXTURE_SIZE = 512;
|
|
4
|
-
|
|
6
|
+
const modelResources = new WeakMap();
|
|
7
|
+
/** Register an instance replacement without transferring borrowed geometry or textures. @internal */
|
|
8
|
+
export function ownModelMaterial(root, material) {
|
|
9
|
+
const resources = modelResources.get(root);
|
|
10
|
+
if (resources === undefined)
|
|
11
|
+
return false;
|
|
12
|
+
resources.materials.add(material);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
/** Resolve placement from bind bounds in the frame containing the imported root, shared by rendering and collider measurement. @internal */
|
|
16
|
+
export function modelPlacementTransform(root, model, bindBounds) {
|
|
17
|
+
let scale = model.scale ?? 1;
|
|
18
|
+
let minY = 0;
|
|
19
|
+
let centerX = 0;
|
|
20
|
+
let centerZ = 0;
|
|
21
|
+
const bounds = model.targetHeight === undefined ? null : bindBounds === undefined ? measureLocalBounds(root) : bindBounds;
|
|
22
|
+
const height = bounds === null ? 0 : bounds.max[1] - bounds.min[1];
|
|
23
|
+
if (bounds !== null && Number.isFinite(height) && height > 0) {
|
|
24
|
+
scale *= model.targetHeight / height;
|
|
25
|
+
minY = bounds.min[1];
|
|
26
|
+
centerX = (bounds.min[0] + bounds.max[0]) / 2;
|
|
27
|
+
centerZ = (bounds.min[2] + bounds.max[2]) / 2;
|
|
28
|
+
}
|
|
29
|
+
else if ((model.anchor ?? "center") === "center" && model.dims !== undefined) {
|
|
30
|
+
minY = model.dims.minY;
|
|
31
|
+
centerX = model.dims.center.x;
|
|
32
|
+
centerZ = model.dims.center.z;
|
|
33
|
+
}
|
|
34
|
+
return { scale, position: [-scale * centerX, (model.y ?? 0) - scale * minY, -scale * centerZ] };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Clone a model with independent pose and materials, retaining shared geometry and textures.
|
|
38
|
+
* @capability model-instance clone a loaded model for independent styling and animation
|
|
39
|
+
*/
|
|
5
40
|
export function cloneModelScene(source, options) {
|
|
6
41
|
const clone = cloneSkinned(source);
|
|
42
|
+
copyModelBindPose(source, clone);
|
|
7
43
|
const shadows = options?.shadows ?? "both";
|
|
8
44
|
const cloneMaterials = options?.cloneMaterials !== false;
|
|
45
|
+
const materials = new Map();
|
|
46
|
+
const sourceSkeletons = [];
|
|
47
|
+
source.traverse((node) => {
|
|
48
|
+
if (node.isSkinnedMesh === true)
|
|
49
|
+
sourceSkeletons.push(node.skeleton);
|
|
50
|
+
});
|
|
51
|
+
const skeletons = new Map();
|
|
52
|
+
let skinnedIndex = 0;
|
|
53
|
+
const cloneMaterial = (source) => {
|
|
54
|
+
let material = materials.get(source);
|
|
55
|
+
if (material === undefined) {
|
|
56
|
+
material = source.clone();
|
|
57
|
+
material.onBeforeCompile = source.onBeforeCompile;
|
|
58
|
+
material.customProgramCacheKey = source.customProgramCacheKey;
|
|
59
|
+
materials.set(source, material);
|
|
60
|
+
}
|
|
61
|
+
return material;
|
|
62
|
+
};
|
|
9
63
|
clone.traverse((node) => {
|
|
10
64
|
const mesh = node;
|
|
11
65
|
if (!mesh.isMesh)
|
|
12
66
|
return;
|
|
67
|
+
if (mesh.isSkinnedMesh === true) {
|
|
68
|
+
const skinned = mesh;
|
|
69
|
+
const sourceSkeleton = sourceSkeletons[skinnedIndex++];
|
|
70
|
+
const shared = skeletons.get(sourceSkeleton);
|
|
71
|
+
if (shared === undefined)
|
|
72
|
+
skeletons.set(sourceSkeleton, skinned.skeleton);
|
|
73
|
+
else
|
|
74
|
+
skinned.skeleton = shared;
|
|
75
|
+
}
|
|
13
76
|
mesh.castShadow = shadows === "cast" || shadows === "both";
|
|
14
77
|
mesh.receiveShadow = shadows === "receive" || shadows === "both";
|
|
15
78
|
if (!cloneMaterials)
|
|
16
79
|
return;
|
|
17
80
|
mesh.material = Array.isArray(mesh.material)
|
|
18
|
-
? mesh.material.map(
|
|
19
|
-
: mesh.material
|
|
81
|
+
? mesh.material.map(cloneMaterial)
|
|
82
|
+
: cloneMaterial(mesh.material);
|
|
20
83
|
});
|
|
84
|
+
modelResources.set(clone, { materials: new Set(materials.values()), skeletons: new Set(skeletons.values()) });
|
|
21
85
|
return clone;
|
|
22
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Release materials and bone textures owned by `cloneModelScene`; shared assets and attached models remain owned by their callers.
|
|
89
|
+
* @capability model-instance release a cloned model without disposing cached geometry or textures
|
|
90
|
+
*/
|
|
91
|
+
export function disposeModelScene(root) {
|
|
92
|
+
const resources = modelResources.get(root);
|
|
93
|
+
if (resources === undefined)
|
|
94
|
+
return;
|
|
95
|
+
for (const material of resources.materials)
|
|
96
|
+
material.dispose();
|
|
97
|
+
// Effect replay can recreate a bone texture on this same scene, so retain its ownership record.
|
|
98
|
+
for (const skeleton of resources.skeletons)
|
|
99
|
+
skeleton.dispose();
|
|
100
|
+
}
|
|
23
101
|
/** @internal */
|
|
24
102
|
export function disposeClonedMaterials(root) {
|
|
25
103
|
const seen = new Set();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
/** Sampling resolution; lower resolutions change thin geometry and alpha-mask raster coverage. */
|
|
3
|
+
export interface ProjectedOverdrawOptions {
|
|
4
|
+
width?: number;
|
|
5
|
+
height?: number;
|
|
6
|
+
}
|
|
7
|
+
/** Projected mesh-fragment overlap, with depth rejection disabled; this is not production early-Z overdraw or GPU timing. */
|
|
8
|
+
export interface ProjectedOverdrawReport {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
sampledPixels: number;
|
|
12
|
+
coveredPixels: number;
|
|
13
|
+
fragmentSamples: number;
|
|
14
|
+
mean: number;
|
|
15
|
+
meanCovered: number;
|
|
16
|
+
max: number;
|
|
17
|
+
meshes: number;
|
|
18
|
+
materialSlots: number;
|
|
19
|
+
/** Native opaque render-queue slots, including masked slots. */
|
|
20
|
+
opaqueSlots: number;
|
|
21
|
+
/** Slots with alphaTest/alphaHash; this can overlap queue counts. */
|
|
22
|
+
maskedSlots: number;
|
|
23
|
+
blendedSlots: number;
|
|
24
|
+
transmissionSlots: number;
|
|
25
|
+
caveats: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
/** Convert finite nonnegative RGBA32F sample counts to overlap statistics. @internal */
|
|
28
|
+
export declare function summarizeProjectedOverdraw(pixels: Float32Array, width: number, height: number): Pick<ProjectedOverdrawReport, "sampledPixels" | "coveredPixels" | "fragmentSamples" | "mean" | "meanCovered" | "max">;
|
|
29
|
+
/**
|
|
30
|
+
* Opt in to a synchronous offscreen count of projected mesh fragments. Native PBR/unlit vertex transforms and alpha discard remain; depth rejection, shadow/refraction passes and postprocessing do not participate.
|
|
31
|
+
* Blended texels count equally, including zero opacity when native alphaTest/alphaHash does not discard them. Pure shader-modifying onBeforeCompile callbacks are retained; render callbacks, custom shader materials, stencil and alpha-to-coverage are rejected.
|
|
32
|
+
* The float readback stalls the GPU. Temporary materials/target are disposed and renderer draw counters/state restored; borrowed assets may become resident while measured.
|
|
33
|
+
* @capability projected-overdraw sample actual mesh/camera fragment overlap with native coverage and explicit depth/blending/transmission limits
|
|
34
|
+
*/
|
|
35
|
+
export declare function measureProjectedOverdraw(renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.Camera, options?: ProjectedOverdrawOptions): ProjectedOverdrawReport;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
function supported(material) {
|
|
3
|
+
const flags = material;
|
|
4
|
+
return flags.isMeshStandardMaterial === true || flags.isMeshBasicMaterial === true;
|
|
5
|
+
}
|
|
6
|
+
/** Convert finite nonnegative RGBA32F sample counts to overlap statistics. @internal */
|
|
7
|
+
export function summarizeProjectedOverdraw(pixels, width, height) {
|
|
8
|
+
if (pixels.length !== width * height * 4)
|
|
9
|
+
throw new Error("Projected-overdraw readback dimensions do not match the RGBA buffer");
|
|
10
|
+
let coveredPixels = 0, fragmentSamples = 0, max = 0;
|
|
11
|
+
for (let index = 0; index < pixels.length; index += 4) {
|
|
12
|
+
const count = pixels[index];
|
|
13
|
+
if (!Number.isFinite(count) || count < 0)
|
|
14
|
+
throw new Error("Projected-overdraw float readback returned invalid samples");
|
|
15
|
+
if (count > 0)
|
|
16
|
+
coveredPixels++;
|
|
17
|
+
fragmentSamples += count;
|
|
18
|
+
max = Math.max(max, count);
|
|
19
|
+
}
|
|
20
|
+
const sampledPixels = width * height;
|
|
21
|
+
return { sampledPixels, coveredPixels, fragmentSamples, mean: fragmentSamples / sampledPixels, meanCovered: coveredPixels === 0 ? 0 : fragmentSamples / coveredPixels, max };
|
|
22
|
+
}
|
|
23
|
+
function countMaterial(source) {
|
|
24
|
+
const material = source.clone();
|
|
25
|
+
const compile = source.onBeforeCompile;
|
|
26
|
+
const key = source.customProgramCacheKey();
|
|
27
|
+
material.onBeforeCompile = (shader, renderer) => {
|
|
28
|
+
compile.call(material, shader, renderer);
|
|
29
|
+
const tail = /#include <dithering_fragment>(\s*})\s*$/;
|
|
30
|
+
if (!tail.test(shader.fragmentShader))
|
|
31
|
+
throw new Error("Projected overdraw requires the native final fragment chunk; this shader callback changes the fragment main tail");
|
|
32
|
+
shader.fragmentShader = shader.fragmentShader.replace(tail, "#include <dithering_fragment>\ngl_FragColor = vec4(1.0);$1");
|
|
33
|
+
};
|
|
34
|
+
material.customProgramCacheKey = () => `${key}|jg-projected-overdraw:v1`;
|
|
35
|
+
material.transparent = true;
|
|
36
|
+
material.blending = THREE.CustomBlending;
|
|
37
|
+
material.blendEquation = THREE.AddEquation;
|
|
38
|
+
material.blendSrc = THREE.OneFactor;
|
|
39
|
+
material.blendDst = THREE.OneFactor;
|
|
40
|
+
material.blendEquationAlpha = THREE.AddEquation;
|
|
41
|
+
material.blendSrcAlpha = THREE.OneFactor;
|
|
42
|
+
material.blendDstAlpha = THREE.OneFactor;
|
|
43
|
+
material.depthTest = false;
|
|
44
|
+
material.depthWrite = false;
|
|
45
|
+
material.colorWrite = true;
|
|
46
|
+
material.forceSinglePass = true;
|
|
47
|
+
material.toneMapped = false;
|
|
48
|
+
material.premultipliedAlpha = false;
|
|
49
|
+
material.alphaToCoverage = false;
|
|
50
|
+
if (material.isMeshPhysicalMaterial)
|
|
51
|
+
material.transmission = 0;
|
|
52
|
+
return material;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Opt in to a synchronous offscreen count of projected mesh fragments. Native PBR/unlit vertex transforms and alpha discard remain; depth rejection, shadow/refraction passes and postprocessing do not participate.
|
|
56
|
+
* Blended texels count equally, including zero opacity when native alphaTest/alphaHash does not discard them. Pure shader-modifying onBeforeCompile callbacks are retained; render callbacks, custom shader materials, stencil and alpha-to-coverage are rejected.
|
|
57
|
+
* The float readback stalls the GPU. Temporary materials/target are disposed and renderer draw counters/state restored; borrowed assets may become resident while measured.
|
|
58
|
+
* @capability projected-overdraw sample actual mesh/camera fragment overlap with native coverage and explicit depth/blending/transmission limits
|
|
59
|
+
*/
|
|
60
|
+
export function measureProjectedOverdraw(renderer, scene, camera, options = {}) {
|
|
61
|
+
const width = options.width ?? 128;
|
|
62
|
+
const height = options.height ?? 128;
|
|
63
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1 || width > 2048 || height > 2048)
|
|
64
|
+
throw new Error("Projected-overdraw resolution must contain integer dimensions from 1 to 2048");
|
|
65
|
+
if (!renderer.extensions.has("EXT_color_buffer_float") || !renderer.extensions.has("EXT_float_blend"))
|
|
66
|
+
throw new Error("Projected overdraw requires RGBA32F rendering/readback and EXT_float_blend; this renderer cannot provide a fragment count");
|
|
67
|
+
if (camera.isArrayCamera)
|
|
68
|
+
throw new Error("Projected overdraw requires one camera projection; measure array-camera views separately");
|
|
69
|
+
if (scene.overrideMaterial !== null)
|
|
70
|
+
throw new Error("Projected overdraw does not support an active scene overrideMaterial");
|
|
71
|
+
const meshes = [];
|
|
72
|
+
const originals = new Map();
|
|
73
|
+
const materials = new Set();
|
|
74
|
+
let materialSlots = 0, opaqueSlots = 0, maskedSlots = 0, blendedSlots = 0, transmissionSlots = 0;
|
|
75
|
+
if (scene.onBeforeRender !== THREE.Object3D.prototype.onBeforeRender || scene.onAfterRender !== THREE.Object3D.prototype.onAfterRender)
|
|
76
|
+
throw new Error("Projected overdraw rejects scene render callbacks");
|
|
77
|
+
scene.traverseVisible(node => {
|
|
78
|
+
if (!node.layers.test(camera.layers))
|
|
79
|
+
return;
|
|
80
|
+
const flags = node;
|
|
81
|
+
if (flags.geometry?.drawRange.count === 0 || (flags.isInstancedMesh && flags.count === 0))
|
|
82
|
+
return;
|
|
83
|
+
if (flags.material !== undefined && (Array.isArray(flags.material) ? flags.material : [flags.material]).every(material => !material.visible))
|
|
84
|
+
return;
|
|
85
|
+
if (flags.isPoints || flags.isLine || flags.isSprite || flags.isBatchedMesh)
|
|
86
|
+
throw new Error(`Projected overdraw does not support ${node.type}`);
|
|
87
|
+
if (!flags.isMesh)
|
|
88
|
+
return;
|
|
89
|
+
if (node.onBeforeRender !== THREE.Object3D.prototype.onBeforeRender || node.onAfterRender !== THREE.Object3D.prototype.onAfterRender)
|
|
90
|
+
throw new Error(`Projected overdraw rejects render callbacks on ${node.name || node.type}`);
|
|
91
|
+
meshes.push(flags);
|
|
92
|
+
originals.set(flags, flags.material);
|
|
93
|
+
for (const material of Array.isArray(flags.material) ? flags.material : [flags.material]) {
|
|
94
|
+
if (!material.visible)
|
|
95
|
+
continue;
|
|
96
|
+
if (!supported(material))
|
|
97
|
+
throw new Error(`Projected overdraw requires native PBR or unlit material; ${material.name || material.type} is unsupported`);
|
|
98
|
+
if (material.onBeforeRender !== THREE.Material.prototype.onBeforeRender)
|
|
99
|
+
throw new Error(`Projected overdraw rejects material render callbacks on ${material.name || material.type}`);
|
|
100
|
+
if (material.stencilWrite || material.alphaToCoverage)
|
|
101
|
+
throw new Error(`Projected overdraw cannot reproduce stencil or multisample alpha coverage on ${material.name || material.type}`);
|
|
102
|
+
materials.add(material);
|
|
103
|
+
materialSlots++;
|
|
104
|
+
if (material.alphaTest > 0 || material.alphaHash)
|
|
105
|
+
maskedSlots++;
|
|
106
|
+
if (material.transparent)
|
|
107
|
+
blendedSlots++;
|
|
108
|
+
else
|
|
109
|
+
opaqueSlots++;
|
|
110
|
+
if (material.transmission > 0)
|
|
111
|
+
transmissionSlots++;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
const target = new THREE.WebGLRenderTarget(width, height, { type: THREE.FloatType, format: THREE.RGBAFormat, minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter, depthBuffer: false, stencilBuffer: false, generateMipmaps: false });
|
|
115
|
+
target.texture.colorSpace = THREE.NoColorSpace;
|
|
116
|
+
const replacements = new Map();
|
|
117
|
+
const userData = new Map([...materials].map(material => [material, { object: material.userData, entries: { ...material.userData } }]));
|
|
118
|
+
const context = renderer.getContext();
|
|
119
|
+
const state = {
|
|
120
|
+
target: renderer.getRenderTarget(), face: renderer.getActiveCubeFace(), mip: renderer.getActiveMipmapLevel(),
|
|
121
|
+
viewport: renderer.getCurrentViewport(new THREE.Vector4()),
|
|
122
|
+
scissor: new THREE.Vector4().fromArray(context.getParameter(context.SCISSOR_BOX)), scissorTest: context.getParameter(context.SCISSOR_TEST),
|
|
123
|
+
clearColor: renderer.getClearColor(new THREE.Color()), clearAlpha: renderer.getClearAlpha(), autoClear: renderer.autoClear,
|
|
124
|
+
autoClearColor: renderer.autoClearColor, autoClearDepth: renderer.autoClearDepth, autoClearStencil: renderer.autoClearStencil,
|
|
125
|
+
xr: renderer.xr.enabled, shadows: renderer.shadowMap.enabled, infoAutoReset: renderer.info.autoReset, render: { ...renderer.info.render },
|
|
126
|
+
background: scene.background, fog: scene.fog, shaderCheck: renderer.debug.checkShaderErrors, shaderError: renderer.debug.onShaderError,
|
|
127
|
+
};
|
|
128
|
+
try {
|
|
129
|
+
const shaderErrors = [];
|
|
130
|
+
renderer.debug.checkShaderErrors = true;
|
|
131
|
+
renderer.debug.onShaderError = (gl, program, vertex, fragment) => { shaderErrors.push([gl.getProgramInfoLog(program), gl.getShaderInfoLog(vertex), gl.getShaderInfoLog(fragment)].filter(Boolean).join("\n")); };
|
|
132
|
+
renderer.xr.enabled = false;
|
|
133
|
+
renderer.shadowMap.enabled = false;
|
|
134
|
+
renderer.info.autoReset = false;
|
|
135
|
+
renderer.autoClear = false;
|
|
136
|
+
renderer.setRenderTarget(target);
|
|
137
|
+
renderer.setClearColor(0x000000, 0.25);
|
|
138
|
+
renderer.clear(true, false, false);
|
|
139
|
+
const probe = new Float32Array(4).fill(NaN);
|
|
140
|
+
renderer.readRenderTargetPixels(target, 0, 0, 1, 1, probe);
|
|
141
|
+
if (!Number.isFinite(probe[3]) || Math.abs(probe[3] - 0.25) > 0.001)
|
|
142
|
+
throw new Error("Projected overdraw RGBA32F readback is unsupported or incomplete");
|
|
143
|
+
renderer.setClearColor(0x000000, 0);
|
|
144
|
+
renderer.clear(true, false, false);
|
|
145
|
+
for (const source of materials)
|
|
146
|
+
replacements.set(source, countMaterial(source));
|
|
147
|
+
for (const mesh of meshes) {
|
|
148
|
+
const source = originals.get(mesh);
|
|
149
|
+
mesh.material = Array.isArray(source) ? source.map(material => replacements.get(material) ?? material) : replacements.get(source) ?? source;
|
|
150
|
+
}
|
|
151
|
+
scene.background = null;
|
|
152
|
+
scene.fog = null;
|
|
153
|
+
renderer.render(scene, camera);
|
|
154
|
+
if (shaderErrors.length > 0)
|
|
155
|
+
throw new Error(`Projected-overdraw shader compilation failed: ${shaderErrors.join("\n")}`);
|
|
156
|
+
const pixels = new Float32Array(width * height * 4).fill(NaN);
|
|
157
|
+
renderer.readRenderTargetPixels(target, 0, 0, width, height, pixels);
|
|
158
|
+
return { width, height, ...summarizeProjectedOverdraw(pixels, width, height), meshes: meshes.length, materialSlots, opaqueSlots, maskedSlots, blendedSlots, transmissionSlots,
|
|
159
|
+
caveats: ["Depth rejection is disabled: projected overlap is an upper bound on production opaque shading, not early-Z overdraw.", "Native alphaTest/alphaHash discard is retained at this sampled resolution; blended texels count equally, including zero opacity.", "Transmission refraction/background, shadow, postprocessing, sky and MSAA passes are excluded. No GPU timing is measured.", "Native vertex transforms and pure shader-modifying material callbacks remain; borrowed assets may be uploaded during this diagnostic."] };
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
for (const [mesh, material] of originals)
|
|
163
|
+
mesh.material = material;
|
|
164
|
+
for (const [material, saved] of userData) {
|
|
165
|
+
material.userData = saved.object;
|
|
166
|
+
for (const key of Object.keys(saved.object))
|
|
167
|
+
if (!(key in saved.entries))
|
|
168
|
+
delete saved.object[key];
|
|
169
|
+
Object.assign(saved.object, saved.entries);
|
|
170
|
+
}
|
|
171
|
+
scene.background = state.background;
|
|
172
|
+
scene.fog = state.fog;
|
|
173
|
+
renderer.xr.enabled = state.xr;
|
|
174
|
+
renderer.shadowMap.enabled = state.shadows;
|
|
175
|
+
renderer.autoClear = state.autoClear;
|
|
176
|
+
renderer.autoClearColor = state.autoClearColor;
|
|
177
|
+
renderer.autoClearDepth = state.autoClearDepth;
|
|
178
|
+
renderer.autoClearStencil = state.autoClearStencil;
|
|
179
|
+
renderer.info.autoReset = state.infoAutoReset;
|
|
180
|
+
renderer.debug.checkShaderErrors = state.shaderCheck;
|
|
181
|
+
renderer.debug.onShaderError = state.shaderError;
|
|
182
|
+
for (const material of replacements.values())
|
|
183
|
+
material.dispose();
|
|
184
|
+
target.dispose();
|
|
185
|
+
if (state.target === null)
|
|
186
|
+
renderer.setRenderTarget(null, state.face, state.mip);
|
|
187
|
+
else {
|
|
188
|
+
const viewport = state.target.viewport.clone(), scissor = state.target.scissor.clone(), scissorTest = state.target.scissorTest;
|
|
189
|
+
state.target.viewport.copy(state.viewport);
|
|
190
|
+
state.target.scissor.copy(state.scissor);
|
|
191
|
+
state.target.scissorTest = state.scissorTest;
|
|
192
|
+
try {
|
|
193
|
+
renderer.setRenderTarget(state.target, state.face, state.mip);
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
state.target.viewport.copy(viewport);
|
|
197
|
+
state.target.scissor.copy(scissor);
|
|
198
|
+
state.target.scissorTest = scissorTest;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
renderer.setClearColor(state.clearColor, state.clearAlpha);
|
|
202
|
+
Object.assign(renderer.info.render, state.render);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { AssetCatalog } from "@jgengine/core/scene/assetCatalog";
|
|
1
|
+
import type { AssetCatalog, ModelAssetRef } from "@jgengine/core/scene/assetCatalog";
|
|
2
2
|
import type { ModelConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
import type { SceneObject } from "@jgengine/core/scene/objectStore";
|
|
3
4
|
import type { GameContextModels } from "@jgengine/core/runtime/gameContext";
|
|
4
5
|
export interface ModelResolveContext {
|
|
5
6
|
seam: "entityModels" | "objectModels" | "scatterModels";
|
|
@@ -16,6 +17,14 @@ export declare function resolveModel(value: string | ModelConfig | undefined, as
|
|
|
16
17
|
* @internal
|
|
17
18
|
*/
|
|
18
19
|
export declare function tryResolveCatalogModel(id: string, assets: AssetCatalog): ModelConfig | undefined;
|
|
20
|
+
/** @internal */
|
|
21
|
+
export interface ObjectModelCacheEntry {
|
|
22
|
+
input: string | ModelConfig | undefined;
|
|
23
|
+
ref: ModelAssetRef | null;
|
|
24
|
+
model: ModelConfig | undefined;
|
|
25
|
+
}
|
|
26
|
+
/** Resolve a live object's catalog and animation, retaining unchanged base model identities. @internal */
|
|
27
|
+
export declare function resolveObjectModel(object: SceneObject, models: Record<string, string | ModelConfig> | undefined, assets: AssetCatalog, cache: Map<string, ObjectModelCacheEntry>): ModelConfig | undefined;
|
|
19
28
|
/**
|
|
20
29
|
* Resolve an entity model plus bone-attachment / kit-of-parts models through the catalog
|
|
21
30
|
* so mounts receive fully-resolved `ModelConfig`s.
|
|
@@ -45,7 +54,8 @@ export declare function resolveModelPlan(assets: AssetCatalog, plan: Record<stri
|
|
|
45
54
|
* The `createGameContext` `models` lookup derived from a playable's render maps: what each entity kind /
|
|
46
55
|
* object catalog id actually renders as, so colliders auto-fit the rendered mesh. Soft-resolves string
|
|
47
56
|
* ids (a missing catalog id keeps the default collider instead of throwing — the render path already
|
|
48
|
-
* reports it), passes inline configs through.
|
|
57
|
+
* reports it), passes inline configs through. Unmapped objects resolve their own catalog id,
|
|
58
|
+
* matching WorldScene; `undefined` when both the catalog and model maps are empty.
|
|
49
59
|
* @internal
|
|
50
60
|
*/
|
|
51
61
|
export declare function contextModels(playable: {
|