@jgengine/shell 0.19.0 → 0.22.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 +364 -0
- package/dist/GameHost.d.ts +12 -1
- package/dist/GameHost.js +14 -1
- package/dist/GamePlayerShell.js +113 -15
- package/dist/Shell3dPresentation.d.ts +2 -2
- package/dist/Shell3dPresentation.js +49 -7
- package/dist/ShellChrome.d.ts +2 -1
- package/dist/ShellChrome.js +22 -6
- package/dist/ShellHudPresentation.d.ts +11 -1
- package/dist/ShellHudPresentation.js +20 -6
- package/dist/audio/audioEngine.d.ts +16 -0
- package/dist/audio/audioEngine.js +178 -89
- package/dist/audio/audioWire.js +2 -2
- package/dist/audio/synthEngine.d.ts +7 -1
- package/dist/audio/synthEngine.js +103 -38
- package/dist/camera/GameCameraRig.js +2 -1
- package/dist/camera/GameFirstPersonCamera.d.ts +2 -2
- package/dist/camera/GameFirstPersonCamera.js +17 -13
- package/dist/camera/cameraRigs.js +8 -2
- 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/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 +12 -5
- package/dist/defineGame.js +25 -10
- package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
- package/dist/devtools/DevtoolsOverlay.js +14 -168
- package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
- package/dist/devtools/DevtoolsRuntime.js +61 -0
- package/dist/devtools/agentBridge.js +1 -1
- 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/drivers/FrameDriver.js +25 -21
- package/dist/drivers/HudOnlyDriver.d.ts +6 -1
- package/dist/drivers/HudOnlyDriver.js +32 -14
- package/dist/environment/EnvironmentScene.js +19 -15
- package/dist/environment/terrainGroundColor.d.ts +5 -4
- package/dist/environment/terrainGroundColor.js +19 -8
- 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 +10 -0
- package/dist/postfx/PostProcessing.js +9 -41
- 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/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 +51 -77
- package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
- package/dist/render/authoredSurfaceMaterial.js +108 -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 +3 -0
- package/dist/render/modelLoad.js +9 -0
- 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 +21 -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.js +76 -11
- package/dist/render/useModelAnimation.d.ts +28 -2
- package/dist/render/useModelAnimation.js +194 -69
- 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 +14 -6
- package/dist/scene/AuthoredScene.js +30 -9
- package/dist/scene/GeneratedAssetRenderer.js +11 -42
- package/dist/scene/GrassFieldRenderer.js +20 -2
- package/dist/settings/SettingsMenu.js +11 -18
- package/dist/settings/SettingsRuntime.d.ts +5 -0
- package/dist/settings/SettingsRuntime.js +29 -2
- 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 +34 -59
- package/dist/structures/BuildingRenderBudget.d.ts +23 -0
- package/dist/structures/BuildingRenderBudget.js +54 -0
- package/dist/structures/GeneratedBuilding.js +13 -8
- 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/terrain/CarvedTerrain.d.ts +2 -1
- package/dist/terrain/CarvedTerrain.js +2 -2
- package/dist/terrain/GrassField.d.ts +3 -1
- package/dist/terrain/GrassField.js +61 -6
- package/dist/terrain/grassGeometry.d.ts +13 -0
- package/dist/terrain/grassGeometry.js +57 -1
- package/dist/terrain/grassMaterial.js +2 -2
- package/dist/terrain/index.d.ts +1 -0
- package/dist/terrain/index.js +1 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
- package/dist/terrain/terrainDetailMaterial.js +7 -7
- package/dist/terrain/terrainMath.d.ts +2 -0
- package/dist/terrain/terrainMath.js +2 -0
- package/dist/terrain/terrainPaintColor.d.ts +11 -0
- package/dist/terrain/terrainPaintColor.js +47 -0
- package/dist/touch/TouchControlsOverlay.js +4 -4
- package/dist/useShellMultiplayerSync.js +4 -0
- 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 +209 -61
- 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 +1 -1
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +23 -2
- package/skills/jgengine/api.md +28 -17
- package/skills/jgengine/recipes/minimal-game.md +7 -2
- package/skills/jgengine-ui/SKILL.md +3 -1
- package/skills/jgengine-ui/api.md +219 -70
- package/skills/jgengine-ui/capabilities.md +119 -1
- package/skills/jgengine-ui/reference-primitives.md +93 -3
- package/skills/jgengine-ui/reference.md +2 -0
- package/skills/jgengine-world/SKILL.md +12 -3
- package/skills/jgengine-world/api.md +201 -58
- package/skills/jgengine-world/capabilities.md +116 -3
- package/skills/jgengine-world/recipes/character-animation.md +31 -8
- package/skills/jgengine-world/recipes/controls.md +4 -0
- package/skills/jgengine-world/recipes/npc-senses.md +12 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
- package/skills/jgengine-world/reference.md +167 -5
- package/skills/jgengine-world/visibility.md +28 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { MATERIAL_TEXTURE_SEMANTICS } from "@jgengine/core/material/materialAsset";
|
|
2
|
+
const legacyMapRoles = ["color", "normal", "roughness", "ao", "metalness", "emissive", "height"];
|
|
3
|
+
const modelMapRoles = [...legacyMapRoles, ...Object.keys(MATERIAL_TEXTURE_SEMANTICS).filter(role => !legacyMapRoles.includes(role))];
|
|
4
|
+
/** Stable loader keys shared by preloading and material application. @internal */
|
|
5
|
+
export function modelMapEntries(maps) {
|
|
6
|
+
const entries = {};
|
|
7
|
+
for (const key of modelMapRoles) {
|
|
8
|
+
if (maps[key] !== undefined)
|
|
9
|
+
entries[key] = maps[key];
|
|
10
|
+
}
|
|
11
|
+
return entries;
|
|
12
|
+
}
|
|
13
|
+
/** Start composed model dependencies together instead of discovering them after each parent commits. @internal */
|
|
14
|
+
export function modelAssetRequests(model) {
|
|
15
|
+
const models = new Set();
|
|
16
|
+
const textureGroups = new Map();
|
|
17
|
+
const visited = new Set();
|
|
18
|
+
const visit = (next) => {
|
|
19
|
+
if (visited.has(next))
|
|
20
|
+
return;
|
|
21
|
+
visited.add(next);
|
|
22
|
+
models.add(next.url);
|
|
23
|
+
if (next.material?.maps !== undefined) {
|
|
24
|
+
const urls = Object.values(modelMapEntries(next.material.maps));
|
|
25
|
+
if (urls.length > 0)
|
|
26
|
+
textureGroups.set(JSON.stringify(urls), urls);
|
|
27
|
+
}
|
|
28
|
+
const referencedIds = new Set(next.materialAssignments?.map(assignment => assignment.materialId));
|
|
29
|
+
for (const asset of next.materialAssets ?? []) {
|
|
30
|
+
if (!referencedIds.has(asset.id))
|
|
31
|
+
continue;
|
|
32
|
+
const urls = Object.values(asset.textures ?? {}).map(metadata => metadata.url);
|
|
33
|
+
if (urls.length > 0)
|
|
34
|
+
textureGroups.set(JSON.stringify(urls), urls);
|
|
35
|
+
}
|
|
36
|
+
for (const child of [...(next.parts ?? []), ...(next.attachments ?? [])]) {
|
|
37
|
+
if (typeof child.model !== "string")
|
|
38
|
+
visit(child.model);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
visit(model);
|
|
42
|
+
return { models: [...models], textureGroups: [...textureGroups.values()] };
|
|
43
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
/** Skin-applied vertices captured before animation, shared by a loaded model's clones. @internal */
|
|
3
|
+
export declare function modelBindPosePositions(mesh: THREE.SkinnedMesh): Float32Array;
|
|
4
|
+
/** Retain the imported pose for normalization and colliders without sampling a live animation. @internal */
|
|
5
|
+
export declare function copyModelBindPose(source: THREE.Object3D, clone: THREE.Object3D): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
const positionsByMesh = new WeakMap();
|
|
3
|
+
const vertex = new THREE.Vector3();
|
|
4
|
+
/** Skin-applied vertices captured before animation, shared by a loaded model's clones. @internal */
|
|
5
|
+
export function modelBindPosePositions(mesh) {
|
|
6
|
+
let positions = positionsByMesh.get(mesh);
|
|
7
|
+
if (positions !== undefined)
|
|
8
|
+
return positions;
|
|
9
|
+
const attribute = mesh.geometry.getAttribute("position");
|
|
10
|
+
const skinned = mesh.geometry.getAttribute("skinIndex") !== undefined && mesh.geometry.getAttribute("skinWeight") !== undefined;
|
|
11
|
+
positions = new Float32Array(attribute.count * 3);
|
|
12
|
+
for (let index = 0; index < attribute.count; index++) {
|
|
13
|
+
if (skinned)
|
|
14
|
+
mesh.getVertexPosition(index, vertex);
|
|
15
|
+
else
|
|
16
|
+
vertex.fromBufferAttribute(attribute, index);
|
|
17
|
+
vertex.toArray(positions, index * 3);
|
|
18
|
+
}
|
|
19
|
+
positionsByMesh.set(mesh, positions);
|
|
20
|
+
return positions;
|
|
21
|
+
}
|
|
22
|
+
/** Retain the imported pose for normalization and colliders without sampling a live animation. @internal */
|
|
23
|
+
export function copyModelBindPose(source, clone) {
|
|
24
|
+
source.updateMatrixWorld(true);
|
|
25
|
+
const positions = [];
|
|
26
|
+
source.traverse((node) => {
|
|
27
|
+
if (node.isSkinnedMesh === true)
|
|
28
|
+
positions.push(modelBindPosePositions(node));
|
|
29
|
+
});
|
|
30
|
+
let index = 0;
|
|
31
|
+
clone.traverse((node) => {
|
|
32
|
+
if (node.isSkinnedMesh === true)
|
|
33
|
+
positionsByMesh.set(node, positions[index++]);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -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
|
|
@@ -111,6 +112,8 @@ export interface ModelLoaderConfig {
|
|
|
111
112
|
export declare function configureModelLoaders(options?: ModelLoaderConfig): Readonly<Required<ModelLoaderConfig>>;
|
|
112
113
|
/** Detects GPU support for the configured KTX2 transcoder after a renderer exists. */
|
|
113
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;
|
|
114
117
|
/**
|
|
115
118
|
* How long the shared GLB loader has been idle, in ms — `0` while any model is still
|
|
116
119
|
* in flight. A capture host reads this to wait for streaming to finish instead of
|
package/dist/render/modelLoad.js
CHANGED
|
@@ -223,6 +223,7 @@ const DEFAULT_DRACO_DECODER_PATH = "https://cdn.jsdelivr.net/npm/three@0.182.0/e
|
|
|
223
223
|
const DEFAULT_KTX2_TRANSCODER_PATH = "https://cdn.jsdelivr.net/npm/three@0.182.0/examples/jsm/libs/basis/";
|
|
224
224
|
let configuredModelLoaders;
|
|
225
225
|
let ktx2Loader;
|
|
226
|
+
const ktx2DetectedRenderers = new WeakMap();
|
|
226
227
|
/** Configures shared Draco and KTX2 loaders. Repeated calls with the same paths are no-ops. */
|
|
227
228
|
export function configureModelLoaders(options = {}) {
|
|
228
229
|
const next = {
|
|
@@ -242,7 +243,15 @@ export function configureModelLoaders(options = {}) {
|
|
|
242
243
|
/** Detects GPU support for the configured KTX2 transcoder after a renderer exists. */
|
|
243
244
|
export function detectKtx2Support(renderer) {
|
|
244
245
|
configureModelLoaders();
|
|
246
|
+
if (ktx2DetectedRenderers.get(renderer) === ktx2Loader)
|
|
247
|
+
return;
|
|
245
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;
|
|
246
255
|
}
|
|
247
256
|
// LoadingManager reports queue transitions, not per-item events: `onStart` fires when the
|
|
248
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: {
|
|
@@ -8,6 +8,7 @@ import { warnOnce } from "@jgengine/core/devtools/warnOnce";
|
|
|
8
8
|
function catalogModelConfig(ref) {
|
|
9
9
|
return {
|
|
10
10
|
url: ref.url,
|
|
11
|
+
...(ref.anchor === undefined ? {} : { anchor: ref.anchor }),
|
|
11
12
|
...(ref.dims === undefined ? {} : { dims: ref.dims }),
|
|
12
13
|
...(ref.collisionMesh === undefined ? {} : { collisionMesh: ref.collisionMesh }),
|
|
13
14
|
animation: "auto",
|
|
@@ -40,6 +41,18 @@ export function tryResolveCatalogModel(id, assets) {
|
|
|
40
41
|
return undefined;
|
|
41
42
|
return catalogModelConfig(ref);
|
|
42
43
|
}
|
|
44
|
+
/** Resolve a live object's catalog and animation, retaining unchanged base model identities. @internal */
|
|
45
|
+
export function resolveObjectModel(object, models, assets, cache) {
|
|
46
|
+
const input = models?.[object.catalogId];
|
|
47
|
+
const ref = assets.resolve(typeof input === "string" ? input : object.catalogId);
|
|
48
|
+
const cached = cache.get(object.catalogId);
|
|
49
|
+
const resolved = cached !== undefined && cached.input === input && cached.ref === ref ? cached.model :
|
|
50
|
+
resolveModel(input, assets, { seam: "objectModels", key: object.catalogId }) ?? tryResolveCatalogModel(object.catalogId, assets);
|
|
51
|
+
if (cached === undefined || cached.model !== resolved || cached.input !== input || cached.ref !== ref) {
|
|
52
|
+
cache.set(object.catalogId, { input, ref, model: resolved });
|
|
53
|
+
}
|
|
54
|
+
return resolved !== undefined && object.animation !== undefined ? { ...resolved, animation: object.animation } : resolved;
|
|
55
|
+
}
|
|
43
56
|
/**
|
|
44
57
|
* Resolve an entity model plus bone-attachment / kit-of-parts models through the catalog
|
|
45
58
|
* so mounts receive fully-resolved `ModelConfig`s.
|
|
@@ -114,17 +127,21 @@ function softModelLookup(map, assets) {
|
|
|
114
127
|
* The `createGameContext` `models` lookup derived from a playable's render maps: what each entity kind /
|
|
115
128
|
* object catalog id actually renders as, so colliders auto-fit the rendered mesh. Soft-resolves string
|
|
116
129
|
* ids (a missing catalog id keeps the default collider instead of throwing — the render path already
|
|
117
|
-
* reports it), passes inline configs through.
|
|
130
|
+
* reports it), passes inline configs through. Unmapped objects resolve their own catalog id,
|
|
131
|
+
* matching WorldScene; `undefined` when both the catalog and model maps are empty.
|
|
118
132
|
* @internal
|
|
119
133
|
*/
|
|
120
134
|
export function contextModels(playable) {
|
|
121
135
|
const entity = softModelLookup(playable.entityModels, playable.game.assets);
|
|
122
|
-
const
|
|
123
|
-
if (entity === undefined &&
|
|
136
|
+
const explicitObject = softModelLookup(playable.objectModels, playable.game.assets);
|
|
137
|
+
if (entity === undefined && explicitObject === undefined && playable.game.assets.ids().length === 0)
|
|
124
138
|
return undefined;
|
|
139
|
+
const object = (key) => playable.objectModels?.[key] === undefined
|
|
140
|
+
? tryResolveCatalogModel(key, playable.game.assets)
|
|
141
|
+
: explicitObject?.(key);
|
|
125
142
|
return {
|
|
126
143
|
...(entity === undefined ? {} : { entity }),
|
|
127
|
-
|
|
144
|
+
object,
|
|
128
145
|
};
|
|
129
146
|
}
|
|
130
147
|
/**
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Object3D } from "three";
|
|
2
|
+
/** A missing or nonunique node reference that a rig author can repair. */
|
|
3
|
+
export interface RigNodeDiagnostic {
|
|
4
|
+
code: "missing-node" | "ambiguous-node";
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
/** A node on this model instance, or a diagnostic instead of an arbitrary attachment target. */
|
|
8
|
+
export type RigNodeResolution = {
|
|
9
|
+
node: Object3D;
|
|
10
|
+
matchedBy: "runtime" | "authored";
|
|
11
|
+
diagnostic?: undefined;
|
|
12
|
+
} | {
|
|
13
|
+
node: undefined;
|
|
14
|
+
matchedBy?: undefined;
|
|
15
|
+
diagnostic: RigNodeDiagnostic;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Resolves a bone or attachment slot by its exact runtime name first, then by the original
|
|
19
|
+
* imported name preserved as `userData.name`. GLTFLoader sanitizes names such as `handslot.r`
|
|
20
|
+
* to `handslotr`; authored references remain usable without renaming the animated hierarchy.
|
|
21
|
+
* Both lookups require a unique target. Call on model load or slot changes, not each frame,
|
|
22
|
+
* and keep the result with that model instance rather than reusing a cached asset's node.
|
|
23
|
+
*
|
|
24
|
+
* @capability imported-rig-node resolve original imported bone and slot names with ambiguity diagnostics
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveRigNode(rig: Object3D, name: string): RigNodeResolution;
|