@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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { reportTextureLoadError } from "@jgengine/core/devtools/textureErrors";
|
|
1
2
|
import { useThree } from "@react-three/fiber";
|
|
2
3
|
import { useEffect } from "react";
|
|
3
4
|
import * as THREE from "three";
|
|
@@ -20,6 +21,27 @@ export function EnvironmentLighting({ source, intensity = 0.35, skyColor = "#87b
|
|
|
20
21
|
let target;
|
|
21
22
|
let loadedTexture;
|
|
22
23
|
let cancelled = false;
|
|
24
|
+
let appliedRotation;
|
|
25
|
+
let appliedRotationValue;
|
|
26
|
+
const previous = scene.environment;
|
|
27
|
+
const previousRotation = scene.environmentRotation;
|
|
28
|
+
const previousIntensity = scene.environmentIntensity;
|
|
29
|
+
const appliedIntensity = source?.intensity ?? intensity;
|
|
30
|
+
scene.environmentIntensity = appliedIntensity;
|
|
31
|
+
const release = () => {
|
|
32
|
+
if (cancelled)
|
|
33
|
+
return;
|
|
34
|
+
cancelled = true;
|
|
35
|
+
if (target !== undefined && scene.environment === target.texture)
|
|
36
|
+
scene.environment = previous;
|
|
37
|
+
if (appliedRotation !== undefined && scene.environmentRotation === appliedRotation && appliedRotation.equals(appliedRotationValue))
|
|
38
|
+
scene.environmentRotation = previousRotation;
|
|
39
|
+
if (scene.environmentIntensity === appliedIntensity)
|
|
40
|
+
scene.environmentIntensity = previousIntensity;
|
|
41
|
+
pmrem.dispose();
|
|
42
|
+
target?.dispose();
|
|
43
|
+
loadedTexture?.dispose();
|
|
44
|
+
};
|
|
23
45
|
const effective = source?.kind === "gradient" ? source : undefined;
|
|
24
46
|
const apply = (next, rotation) => {
|
|
25
47
|
if (cancelled) {
|
|
@@ -28,44 +50,61 @@ export function EnvironmentLighting({ source, intensity = 0.35, skyColor = "#87b
|
|
|
28
50
|
}
|
|
29
51
|
target = next;
|
|
30
52
|
scene.environment = next.texture;
|
|
31
|
-
if (rotation !== undefined)
|
|
32
|
-
|
|
53
|
+
if (rotation !== undefined) {
|
|
54
|
+
appliedRotation = new THREE.Euler(0, rotation, 0);
|
|
55
|
+
appliedRotationValue = appliedRotation.clone();
|
|
56
|
+
scene.environmentRotation = appliedRotation;
|
|
57
|
+
}
|
|
33
58
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
try {
|
|
60
|
+
if (source?.kind === "hdri") {
|
|
61
|
+
const Loader = /\.exr(?:$|\?)/i.test(source.url) ? EXRLoader : RGBELoader;
|
|
62
|
+
new Loader().loadAsync(source.url).then((texture) => {
|
|
63
|
+
if (cancelled) {
|
|
64
|
+
texture.dispose();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
loadedTexture = texture;
|
|
68
|
+
texture.mapping = THREE.EquirectangularReflectionMapping;
|
|
69
|
+
apply(pmrem.fromEquirectangular(texture), source.rotation);
|
|
70
|
+
}).catch((error) => {
|
|
71
|
+
if (cancelled)
|
|
72
|
+
return;
|
|
73
|
+
reportTextureLoadError(source.url);
|
|
74
|
+
console.warn(`[jgengine] environment illumination failed: ${source.url}`, error);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
else if (source?.kind === "cube") {
|
|
78
|
+
new THREE.CubeTextureLoader().loadAsync(source.urls).then((texture) => {
|
|
79
|
+
if (cancelled) {
|
|
80
|
+
texture.dispose();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
loadedTexture = texture;
|
|
84
|
+
apply(pmrem.fromCubemap(texture));
|
|
85
|
+
}).catch((error) => {
|
|
86
|
+
if (cancelled)
|
|
87
|
+
return;
|
|
88
|
+
for (const url of source.urls)
|
|
89
|
+
reportTextureLoadError(url);
|
|
90
|
+
console.warn(`[jgengine] cube environment illumination failed: ${source.urls.join(", ")}`, error);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const probe = buildSkyProbeScene({ skyColor: effective?.sky ?? skyColor, groundColor: effective?.ground ?? groundColor, sunDirection, sunColor: effective?.sun ?? sunColor });
|
|
95
|
+
try {
|
|
96
|
+
apply(pmrem.fromScene(probe, 0.04));
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
disposeScene(probe);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
49
102
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
disposeScene(probe);
|
|
103
|
+
catch (error) {
|
|
104
|
+
release();
|
|
105
|
+
throw error;
|
|
54
106
|
}
|
|
55
|
-
|
|
56
|
-
const previousIntensity = sceneWithIntensity.environmentIntensity;
|
|
57
|
-
sceneWithIntensity.environmentIntensity = source?.intensity ?? intensity;
|
|
58
|
-
return () => {
|
|
59
|
-
cancelled = true;
|
|
60
|
-
pmrem.dispose();
|
|
61
|
-
target?.dispose();
|
|
62
|
-
loadedTexture?.dispose();
|
|
63
|
-
scene.environment = previous;
|
|
64
|
-
if (previousRotation !== undefined)
|
|
65
|
-
scene.environmentRotation = previousRotation;
|
|
66
|
-
if (previousIntensity !== undefined)
|
|
67
|
-
sceneWithIntensity.environmentIntensity = previousIntensity;
|
|
68
|
-
};
|
|
107
|
+
return release;
|
|
69
108
|
}, [gl, scene, source, intensity, skyColor, groundColor, sunColor, sunDirection?.[0], sunDirection?.[1], sunDirection?.[2]]);
|
|
70
109
|
return null;
|
|
71
110
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type HairCardAuthoring } from "@jgengine/core/material/hairCards";
|
|
2
|
+
import type { MaterialAsset } from "@jgengine/core/material/materialAsset";
|
|
3
|
+
/** Render authored guide ribbons with explicit UVs and strand tangents; motion and collision remain caller-owned. */
|
|
4
|
+
export declare function HairCards({ cards, material }: {
|
|
5
|
+
cards: HairCardAuthoring;
|
|
6
|
+
material: MaterialAsset;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import * as THREE from "three";
|
|
4
|
+
import { buildHairCards } from "@jgengine/core/material/hairCards";
|
|
5
|
+
import { MaterialAssetSurface } from "./materialAsset.js";
|
|
6
|
+
import { useDisposable } from "./useDisposable.js";
|
|
7
|
+
/** Render authored guide ribbons with explicit UVs and strand tangents; motion and collision remain caller-owned. */
|
|
8
|
+
export function HairCards({ cards, material }) {
|
|
9
|
+
const key = JSON.stringify(cards);
|
|
10
|
+
const mesh = useMemo(() => buildHairCards(cards), [key]);
|
|
11
|
+
const geometry = useDisposable(() => {
|
|
12
|
+
const next = new THREE.BufferGeometry();
|
|
13
|
+
next.setAttribute("position", new THREE.Float32BufferAttribute(mesh.positions, 3));
|
|
14
|
+
next.setAttribute("normal", new THREE.Float32BufferAttribute(mesh.normals, 3));
|
|
15
|
+
next.setAttribute("tangent", new THREE.Float32BufferAttribute(mesh.tangents, 4));
|
|
16
|
+
next.setAttribute("uv", new THREE.Float32BufferAttribute(mesh.uvs, 2));
|
|
17
|
+
next.setIndex(mesh.indices);
|
|
18
|
+
next.computeBoundingBox();
|
|
19
|
+
next.computeBoundingSphere();
|
|
20
|
+
return next;
|
|
21
|
+
}, [mesh]);
|
|
22
|
+
return _jsx("mesh", { geometry: geometry, castShadow: true, receiveShadow: true, children: _jsx(MaterialAssetSurface, { asset: material }) });
|
|
23
|
+
}
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useFrame, useLoader } from "@react-three/fiber";
|
|
3
|
-
import {
|
|
4
|
-
import { Component, Suspense, useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { useFrame, useLoader, useThree } from "@react-three/fiber";
|
|
3
|
+
import { Component, Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRef } from "react";
|
|
5
4
|
import * as THREE from "three";
|
|
5
|
+
import { parseModelHiddenNodes } from "@jgengine/core/editor/modelNodeVisibility";
|
|
6
6
|
import { createSpriteClipPlayer } from "@jgengine/core/render/sprite2d";
|
|
7
7
|
import { reportFallbackSeam } from "@jgengine/core/devtools/fallbackSeams";
|
|
8
8
|
import { useOptionalGameContext } from "@jgengine/react/provider";
|
|
9
|
-
import { sharedGltfLoader } from "./modelLoad.js";
|
|
9
|
+
import { detectKtx2Support, sharedGltfLoader } from "./modelLoad.js";
|
|
10
|
+
import { modelAssetRequests } from "./modelAssets.js";
|
|
10
11
|
import { measureLocalBounds, reportMeasuredBounds } from "./measureBounds.js";
|
|
11
12
|
import { measureLocalCollisionTriangles, reportMeasuredCollisionMesh } from "./measureCollisionMesh.js";
|
|
12
|
-
import {
|
|
13
|
+
import { useModelInstance } from "./useModelInstance.js";
|
|
14
|
+
import { applyMaterialAssignments, useModelMaterialTextures, MaterialTextureLoader, validateMaterialAssignmentTargets } from "./materialAsset.js";
|
|
13
15
|
import { useFootIk } from "./useFootIk.js";
|
|
16
|
+
import { resolveRigNode } from "./rigNode.js";
|
|
14
17
|
import { PartMotionRig } from "./PartMotion.js";
|
|
18
|
+
import { syncSpriteFrame } from "./spriteRender.js";
|
|
15
19
|
import { applyMaterialOverride } from "../materialOverride.js";
|
|
16
|
-
import { applyPaintTextureToMaterials, cacheStandardMaterials, cloneModelScene, createPaintCanvas,
|
|
20
|
+
import { applyPaintTextureToMaterials, cacheStandardMaterials, cloneModelScene, createPaintCanvas, disposeModelScene, ownModelMaterial, disposePaintCanvas, syncPaintCanvas, } from "./modelRender.js";
|
|
17
21
|
export function EntitySprite({ sprite }) {
|
|
22
|
+
const invalidate = useThree((state) => state.invalidate);
|
|
18
23
|
const texture = useLoader(THREE.TextureLoader, sprite.clip?.atlas.image ?? sprite.url);
|
|
19
24
|
const map = useMemo(() => texture.clone(), [texture]);
|
|
20
25
|
const player = useMemo(() => (sprite.clip === undefined ? null : createSpriteClipPlayer(sprite.clip.atlas)), [sprite.clip]);
|
|
@@ -28,7 +33,11 @@ export function EntitySprite({ sprite }) {
|
|
|
28
33
|
useEffect(() => {
|
|
29
34
|
if (player !== null && animation !== undefined)
|
|
30
35
|
player.play(animation);
|
|
31
|
-
|
|
36
|
+
const frame = player?.frame();
|
|
37
|
+
if (frame !== undefined && sprite.clip !== undefined)
|
|
38
|
+
syncSpriteFrame(map, frame, sprite.clip.atlas.size);
|
|
39
|
+
invalidate();
|
|
40
|
+
}, [player, animation, map, sprite.clip, invalidate]);
|
|
32
41
|
useFrame((_state, delta) => {
|
|
33
42
|
if (player === null || sprite.clip === undefined)
|
|
34
43
|
return;
|
|
@@ -36,10 +45,7 @@ export function EntitySprite({ sprite }) {
|
|
|
36
45
|
const frame = player.frame();
|
|
37
46
|
if (frame === undefined)
|
|
38
47
|
return;
|
|
39
|
-
|
|
40
|
-
map.repeat.set(frame.w / atlasWidth, frame.h / atlasHeight);
|
|
41
|
-
map.offset.set(frame.x / atlasWidth, 1 - (frame.y + frame.h) / atlasHeight);
|
|
42
|
-
map.needsUpdate = true;
|
|
48
|
+
syncSpriteFrame(map, frame, sprite.clip.atlas.size);
|
|
43
49
|
});
|
|
44
50
|
useEffect(() => {
|
|
45
51
|
if (player === null) {
|
|
@@ -97,13 +103,14 @@ function BoneAttachment({ rig, model, slot, position, rotation, scale, }) {
|
|
|
97
103
|
const rz = rotation?.[2] ?? 0;
|
|
98
104
|
const s = scale ?? 1;
|
|
99
105
|
useEffect(() => {
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
106
|
+
const resolved = resolveRigNode(rig, slot);
|
|
107
|
+
if (resolved.node === undefined) {
|
|
102
108
|
if (typeof console !== "undefined") {
|
|
103
|
-
console.warn(`[jgengine] entityModels attachment:
|
|
109
|
+
console.warn(`[jgengine] entityModels attachment: ${resolved.diagnostic.message}`);
|
|
104
110
|
}
|
|
105
111
|
return;
|
|
106
112
|
}
|
|
113
|
+
const bone = resolved.node;
|
|
107
114
|
weaponScene.position.set(px, py, pz);
|
|
108
115
|
weaponScene.rotation.set(rx, ry, rz);
|
|
109
116
|
weaponScene.scale.setScalar(s);
|
|
@@ -112,80 +119,47 @@ function BoneAttachment({ rig, model, slot, position, rotation, scale, }) {
|
|
|
112
119
|
bone.remove(weaponScene);
|
|
113
120
|
};
|
|
114
121
|
}, [rig, weaponScene, slot, px, py, pz, rx, ry, rz, s]);
|
|
115
|
-
useEffect(() => () =>
|
|
122
|
+
useEffect(() => () => disposeModelScene(weaponScene), [weaponScene]);
|
|
116
123
|
return null;
|
|
117
124
|
}
|
|
118
125
|
/** Renders a static kit-of-parts child at a fixed local offset under the parent model — no bone/rig lookup, unlike `BoneAttachment`. The nested `EntityModel` still applies its own `dims`/anchor centering, so scale/pivot sanity carries over per part. */
|
|
119
126
|
function ModelPartGroup({ model, position, rotation, scale, }) {
|
|
120
127
|
return (_jsx("group", { position: position ?? [0, 0, 0], rotation: rotation ?? [0, 0, 0], scale: scale ?? 1, children: _jsx(IsolatedModelPart, { model: model, children: _jsx(EntityModel, { model: model }) }) }));
|
|
121
128
|
}
|
|
122
|
-
function ModelMaterialMapsApplier({ scene, maps }) {
|
|
123
|
-
const entries = useMemo(() => {
|
|
124
|
-
const record = {};
|
|
125
|
-
if (maps.color !== undefined)
|
|
126
|
-
record.color = maps.color;
|
|
127
|
-
if (maps.normal !== undefined)
|
|
128
|
-
record.normal = maps.normal;
|
|
129
|
-
if (maps.roughness !== undefined)
|
|
130
|
-
record.roughness = maps.roughness;
|
|
131
|
-
if (maps.ao !== undefined)
|
|
132
|
-
record.ao = maps.ao;
|
|
133
|
-
if (maps.metalness !== undefined)
|
|
134
|
-
record.metalness = maps.metalness;
|
|
135
|
-
if (maps.emissive !== undefined)
|
|
136
|
-
record.emissive = maps.emissive;
|
|
137
|
-
if (maps.height !== undefined)
|
|
138
|
-
record.height = maps.height;
|
|
139
|
-
return record;
|
|
140
|
-
}, [maps.color, maps.normal, maps.roughness, maps.ao, maps.metalness, maps.emissive, maps.height]);
|
|
141
|
-
const textures = useTexture(entries);
|
|
142
|
-
useEffect(() => {
|
|
143
|
-
if (textures.color !== undefined)
|
|
144
|
-
textures.color.colorSpace = THREE.SRGBColorSpace;
|
|
145
|
-
applyMaterialOverride(scene, {}, { clone: false, textures });
|
|
146
|
-
}, [scene, textures]);
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
129
|
export function EntityModel({ model, instanceId, measure, }) {
|
|
150
|
-
|
|
130
|
+
parseModelHiddenNodes(model.hiddenNodes);
|
|
131
|
+
const renderer = useThree(state => state.gl);
|
|
132
|
+
if (renderer.isWebGLRenderer)
|
|
133
|
+
detectKtx2Support(renderer);
|
|
134
|
+
const assets = modelAssetRequests(model);
|
|
135
|
+
for (const url of assets.models)
|
|
136
|
+
useLoader.preload(sharedGltfLoader, url);
|
|
151
137
|
// Optional, not required: measured bounds and paint strokes are live-world extras, and a model
|
|
152
138
|
// that threw without a running game could not be inspected outside one — which is how a broken
|
|
153
139
|
// composition stayed undiagnosable in `EntityPreview` (#1588).
|
|
154
140
|
const ctx = useOptionalGameContext();
|
|
155
141
|
const material = model.material;
|
|
142
|
+
const materialAssetKey = JSON.stringify((model.materialAssets ?? []).filter(asset => model.materialAssignments?.some(assignment => assignment.materialId === asset.id)));
|
|
143
|
+
const materialAssets = useMemo(() => (model.materialAssets ?? []).filter(asset => model.materialAssignments?.some(assignment => assignment.materialId === asset.id)), [materialAssetKey]);
|
|
144
|
+
const materialAssignments = model.materialAssignments;
|
|
145
|
+
const assignmentSource = useLoader(sharedGltfLoader, materialAssignments?.length ? [model.url] : [])[0]?.scene;
|
|
146
|
+
const assignmentKey = JSON.stringify(materialAssignments);
|
|
147
|
+
useMemo(() => {
|
|
148
|
+
if (assignmentSource !== undefined)
|
|
149
|
+
validateMaterialAssignmentTargets(assignmentSource, materialAssets, materialAssignments, { baseAlphaMode: material?.alphaMode });
|
|
150
|
+
}, [assignmentSource, materialAssets, assignmentKey, material?.alphaMode]);
|
|
151
|
+
for (const urls of assets.textureGroups)
|
|
152
|
+
useLoader.preload(MaterialTextureLoader, urls);
|
|
153
|
+
const materialTextures = useModelMaterialTextures(model);
|
|
156
154
|
const baseY = model.y ?? 0;
|
|
157
155
|
const dims = model.dims;
|
|
158
|
-
const
|
|
159
|
-
const scene = useMemo(() => {
|
|
160
|
-
const cloned = cloneModelScene(gltf.scene, { shadows });
|
|
156
|
+
const configure = useCallback((content) => {
|
|
161
157
|
if (material !== undefined)
|
|
162
|
-
applyMaterialOverride(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return null;
|
|
168
|
-
const box = new THREE.Box3().setFromObject(scene);
|
|
169
|
-
const height = box.max.y - box.min.y;
|
|
170
|
-
if (!Number.isFinite(height) || height <= 0)
|
|
171
|
-
return null;
|
|
172
|
-
return {
|
|
173
|
-
normalize: model.targetHeight / height,
|
|
174
|
-
minY: box.min.y,
|
|
175
|
-
centerX: (box.min.x + box.max.x) / 2,
|
|
176
|
-
centerZ: (box.min.z + box.max.z) / 2,
|
|
177
|
-
};
|
|
178
|
-
}, [scene, model.targetHeight]);
|
|
179
|
-
const scale = (model.scale ?? 1) * (measured?.normalize ?? 1);
|
|
180
|
-
const centered = (model.anchor ?? "center") === "center" && dims !== undefined;
|
|
181
|
-
const position = measured !== null
|
|
182
|
-
? [-scale * measured.centerX, baseY - scale * measured.minY, -scale * measured.centerZ]
|
|
183
|
-
: centered
|
|
184
|
-
? [-scale * dims.center.x, baseY - scale * dims.minY, -scale * dims.center.z]
|
|
185
|
-
: [0, baseY, 0];
|
|
186
|
-
useEffect(() => () => {
|
|
187
|
-
disposeClonedMaterials(scene);
|
|
188
|
-
}, [scene]);
|
|
158
|
+
applyMaterialOverride(content, material, { clone: false, textures: materialTextures.global, ownMaterial: replacement => ownModelMaterial(content, replacement) });
|
|
159
|
+
if (materialAssignments !== undefined)
|
|
160
|
+
applyMaterialAssignments(content, materialAssets ?? [], materialAssignments, materialTextures.assets);
|
|
161
|
+
}, [material, materialTextures, materialAssets, materialAssignments]);
|
|
162
|
+
const { scene, scale, position } = useModelInstance(model, { instanceId, configure });
|
|
189
163
|
// A model without index-measured dims can't drive the fitted-collider path, so report the live
|
|
190
164
|
// measurement (the primitive mounts with only this uniform scale + position) instead of letting
|
|
191
165
|
// the kind fall back to a fixed-size box. Index dims keep priority — skip when they exist.
|
|
@@ -193,7 +167,8 @@ export function EntityModel({ model, instanceId, measure, }) {
|
|
|
193
167
|
const measureKey = measure?.key;
|
|
194
168
|
const dimsMaxY = dims?.maxY;
|
|
195
169
|
const [positionX, positionY, positionZ] = position;
|
|
196
|
-
|
|
170
|
+
// Measure before the instance hook's passive animation effects can move imported root nodes.
|
|
171
|
+
useLayoutEffect(() => {
|
|
197
172
|
if (ctx === null || measureTarget === undefined || measureKey === undefined || dimsMaxY !== undefined)
|
|
198
173
|
return;
|
|
199
174
|
const raw = measureLocalBounds(scene);
|
|
@@ -207,7 +182,7 @@ export function EntityModel({ model, instanceId, measure, }) {
|
|
|
207
182
|
}, [ctx, scene, scale, positionX, positionY, positionZ, measureTarget, measureKey, dimsMaxY]);
|
|
208
183
|
// Runs with or without index dims: the loaded geometry upgrades whatever box the kind resolved
|
|
209
184
|
// (fitted or measured) to a hitbox that raycasts the model's own triangles.
|
|
210
|
-
|
|
185
|
+
useLayoutEffect(() => {
|
|
211
186
|
if (ctx === null || measureTarget === undefined || measureKey === undefined)
|
|
212
187
|
return;
|
|
213
188
|
const triangles = measureLocalCollisionTriangles(scene, {
|
|
@@ -217,7 +192,6 @@ export function EntityModel({ model, instanceId, measure, }) {
|
|
|
217
192
|
if (triangles !== null)
|
|
218
193
|
reportMeasuredCollisionMesh(ctx, measureTarget, measureKey, triangles);
|
|
219
194
|
}, [ctx, scene, scale, positionX, positionY, positionZ, measureTarget, measureKey]);
|
|
220
|
-
useModelAnimation(scene, gltf.animations, model.animation, instanceId);
|
|
221
195
|
useFootIk(scene, model.ik, ctx, instanceId, baseY);
|
|
222
196
|
const paintCanvasRef = useRef(null);
|
|
223
197
|
const paintDrawnCountRef = useRef(0);
|
|
@@ -255,7 +229,7 @@ export function EntityModel({ model, instanceId, measure, }) {
|
|
|
255
229
|
}
|
|
256
230
|
paintDrawnCountRef.current = syncPaintCanvas(paintCanvasRef.current, cache.seedColor, strokes, paintDrawnCountRef.current);
|
|
257
231
|
});
|
|
258
|
-
const base = (_jsxs(_Fragment, { children: [_jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] }),
|
|
232
|
+
const base = (_jsxs(_Fragment, { children: [_jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] }), (model.attachments ?? []).map((attachment, index) => typeof attachment.model === "string" ? null : (_jsx(IsolatedModelPart, { model: attachment.model, children: _jsx(BoneAttachment, { rig: scene, model: attachment.model, slot: attachment.slot, position: attachment.position, rotation: attachment.rotation, scale: attachment.scale }) }, `${attachment.slot}-${index}`)))] }));
|
|
259
233
|
const parts = model.parts ?? [];
|
|
260
234
|
// Any role-tagged part switches the composition onto the procedural part-motion rig:
|
|
261
235
|
// root bob/flinch/topple plus per-role limb swing, driven from the entity's live state.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type { ModelMaterialOverride } from "@jgengine/core/game/playableGame";
|
|
3
|
+
import { type MaterialOverrideTextures } from "../materialOverride.js";
|
|
4
|
+
/** Serializable PBR settings for an authored primitive surface. Map roles use the model-material contract. */
|
|
5
|
+
export interface AuthoredSurfaceConfig extends Omit<ModelMaterialOverride, "anisotropy" | "normalScale"> {
|
|
6
|
+
/** Physical metres per texture tile on unit boxes/cylinders, including instance and parent scale. Omit to retain authored UVs. */
|
|
7
|
+
repeatMetres?: number;
|
|
8
|
+
/** Texture sampler policy on owned clones; defaults to repeat on both axes and anisotropy 1. */
|
|
9
|
+
wrapping?: "repeat" | "clamp" | "mirror";
|
|
10
|
+
anisotropy?: number;
|
|
11
|
+
/** Physical directional highlight strength [0,1]; `anisotropy` remains the legacy texture sampler setting. */
|
|
12
|
+
surfaceAnisotropy?: number;
|
|
13
|
+
normalScale?: readonly [number, number];
|
|
14
|
+
/** Normalized [0,1] height maps displace in local shape units (Three defaults: scale 1, bias 0). */
|
|
15
|
+
displacementScale?: number;
|
|
16
|
+
displacementBias?: number;
|
|
17
|
+
transparent?: boolean;
|
|
18
|
+
opacity?: number;
|
|
19
|
+
depthWrite?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** Primitive UV layout whose physical dimensions drive `repeatMetres`. */
|
|
22
|
+
export type SurfaceShape = "box" | "cylinder";
|
|
23
|
+
/** Stable material identity, independent of object/map property insertion order. @internal */
|
|
24
|
+
export declare function authoredSurfaceKey(config: AuthoredSurfaceConfig): string;
|
|
25
|
+
/** Configure owned map clones without changing textures retained in a loader cache. @internal */
|
|
26
|
+
export declare function authoredSurfaceTextures(entries: Record<string, string>, sources: readonly THREE.Texture[], config?: AuthoredSurfaceConfig): MaterialOverrideTextures;
|
|
27
|
+
/** Apply PBR settings and optional metre UVs to an owned standard material. @internal */
|
|
28
|
+
export declare function configureAuthoredSurface(material: THREE.MeshStandardMaterial, config: AuthoredSurfaceConfig, textures: MaterialOverrideTextures, shape: SurfaceShape): void;
|
|
29
|
+
/**
|
|
30
|
+
* Load declared surface-map roles and own the material/map clones (cached textures remain untouched).
|
|
31
|
+
* Colour, emissive, sheen-colour and specular-colour maps are sRGB; numeric maps are linear. Cylinder side metres use the RMS ellipse circumference approximation.
|
|
32
|
+
* Static batches and custom meshes share this lifecycle.
|
|
33
|
+
* @capability authored-surface-material render declared PBR maps with colour-space handling and metre-scale box/cylinder UVs
|
|
34
|
+
*/
|
|
35
|
+
export declare function useAuthoredSurfaceMaterial(config: AuthoredSurfaceConfig, shape?: SurfaceShape): THREE.MeshStandardMaterial;
|
|
36
|
+
/**
|
|
37
|
+
* Mount an owned PBR surface material on a mesh.
|
|
38
|
+
* @capability authored-surface-material mount an owned PBR surface material on a mesh
|
|
39
|
+
*/
|
|
40
|
+
export declare function AuthoredSurfaceMaterial({ surface, shape }: {
|
|
41
|
+
surface: AuthoredSurfaceConfig;
|
|
42
|
+
shape?: SurfaceShape;
|
|
43
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useLoader } from "@react-three/fiber";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { MATERIAL_TEXTURE_SEMANTICS } from "@jgengine/core/material/materialAsset";
|
|
6
|
+
import { applyMaterialOverrideToMaterial, requiresPhysicalMaterial } from "../materialOverride.js";
|
|
7
|
+
import { modelMapEntries } from "./modelAssets.js";
|
|
8
|
+
import { useDisposable } from "./useDisposable.js";
|
|
9
|
+
/** Stable material identity, independent of object/map property insertion order. @internal */
|
|
10
|
+
export function authoredSurfaceKey(config) {
|
|
11
|
+
return JSON.stringify(Object.entries({ ...config, maps: modelMapEntries(config.maps ?? {}) }).sort(([a], [b]) => a.localeCompare(b)));
|
|
12
|
+
}
|
|
13
|
+
/** Configure owned map clones without changing textures retained in a loader cache. @internal */
|
|
14
|
+
export function authoredSurfaceTextures(entries, sources, config = {}) {
|
|
15
|
+
const wrapping = config.wrapping === "clamp" ? THREE.ClampToEdgeWrapping : config.wrapping === "mirror" ? THREE.MirroredRepeatWrapping : THREE.RepeatWrapping;
|
|
16
|
+
return Object.fromEntries(Object.keys(entries).map((role, index) => {
|
|
17
|
+
const texture = sources[index].clone();
|
|
18
|
+
texture.colorSpace = MATERIAL_TEXTURE_SEMANTICS[role]?.colorSpace === "srgb" ? THREE.SRGBColorSpace : THREE.NoColorSpace;
|
|
19
|
+
texture.wrapS = wrapping;
|
|
20
|
+
texture.wrapT = wrapping;
|
|
21
|
+
texture.anisotropy = config.anisotropy ?? 1;
|
|
22
|
+
texture.needsUpdate = true;
|
|
23
|
+
return [role, texture];
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
/** Apply PBR settings and optional metre UVs to an owned standard material. @internal */
|
|
27
|
+
export function configureAuthoredSurface(material, config, textures, shape) {
|
|
28
|
+
const tile = config.repeatMetres;
|
|
29
|
+
if (tile !== undefined && (!Number.isFinite(tile) || tile <= 0))
|
|
30
|
+
throw new Error("repeatMetres must be finite and positive");
|
|
31
|
+
if (config.normalScale?.some(value => !Number.isFinite(value)))
|
|
32
|
+
throw new Error("normalScale must be finite");
|
|
33
|
+
if (config.anisotropy !== undefined && (!Number.isFinite(config.anisotropy) || config.anisotropy < 1))
|
|
34
|
+
throw new Error("anisotropy must be finite and >= 1");
|
|
35
|
+
if ([config.displacementScale, config.displacementBias].some(value => value !== undefined && !Number.isFinite(value)))
|
|
36
|
+
throw new Error("displacement settings must be finite");
|
|
37
|
+
const { anisotropy: _samplerAnisotropy, surfaceAnisotropy, ...surface } = config;
|
|
38
|
+
if (requiresPhysicalMaterial({ ...surface, normalScale: surface.normalScale === undefined ? undefined : [...surface.normalScale], anisotropy: surfaceAnisotropy }, textures) && !material.isMeshPhysicalMaterial)
|
|
39
|
+
throw new Error("Physical authored surface settings require MeshPhysicalMaterial");
|
|
40
|
+
applyMaterialOverrideToMaterial(material, { ...surface, normalScale: surface.normalScale === undefined ? undefined : [...surface.normalScale], anisotropy: surfaceAnisotropy }, false, textures);
|
|
41
|
+
if (config.displacementScale !== undefined)
|
|
42
|
+
material.displacementScale = config.displacementScale;
|
|
43
|
+
if (config.displacementBias !== undefined)
|
|
44
|
+
material.displacementBias = config.displacementBias;
|
|
45
|
+
if (config.transparent !== undefined)
|
|
46
|
+
material.transparent = config.transparent;
|
|
47
|
+
if (config.opacity !== undefined)
|
|
48
|
+
material.opacity = config.opacity;
|
|
49
|
+
if (config.depthWrite !== undefined)
|
|
50
|
+
material.depthWrite = config.depthWrite;
|
|
51
|
+
if (config.normalScale !== undefined)
|
|
52
|
+
material.normalScale.set(...config.normalScale);
|
|
53
|
+
if (tile === undefined)
|
|
54
|
+
return;
|
|
55
|
+
const previousCompile = material.onBeforeCompile;
|
|
56
|
+
const previousKey = material.customProgramCacheKey();
|
|
57
|
+
material.onBeforeCompile = (shader, renderer) => {
|
|
58
|
+
previousCompile.call(material, shader, renderer);
|
|
59
|
+
shader.vertexShader = shader.vertexShader.replace("#include <uv_vertex>", `#include <uv_vertex>
|
|
60
|
+
mat4 surfaceTransform = modelMatrix;
|
|
61
|
+
#ifdef USE_INSTANCING
|
|
62
|
+
surfaceTransform = modelMatrix * instanceMatrix;
|
|
63
|
+
#endif
|
|
64
|
+
vec3 surfaceExtent = vec3(length(surfaceTransform[0].xyz), length(surfaceTransform[1].xyz), length(surfaceTransform[2].xyz));
|
|
65
|
+
vec2 surfaceRepeat = ${shape === "box"
|
|
66
|
+
? "abs(normal.x) > 0.5 ? surfaceExtent.zy : abs(normal.y) > 0.5 ? surfaceExtent.xz : surfaceExtent.xy"
|
|
67
|
+
: "abs(normal.y) > 0.5 ? surfaceExtent.xz * 2.0 : vec2(6.28318530718 * sqrt((surfaceExtent.x * surfaceExtent.x + surfaceExtent.z * surfaceExtent.z) * 0.5), surfaceExtent.y)"};
|
|
68
|
+
surfaceRepeat /= ${tile.toExponential(8)};
|
|
69
|
+
${Object.entries({ MAP: "vMapUv", NORMALMAP: "vNormalMapUv", ROUGHNESSMAP: "vRoughnessMapUv", AOMAP: "vAoMapUv", METALNESSMAP: "vMetalnessMapUv", EMISSIVEMAP: "vEmissiveMapUv", DISPLACEMENTMAP: "vDisplacementMapUv", ALPHAMAP: "vAlphaMapUv", SHEENCOLORMAP: "vSheenColorMapUv", SHEENROUGHNESSMAP: "vSheenRoughnessMapUv", ANISOTROPYMAP: "vAnisotropyMapUv", CLEARCOATMAP: "vClearcoatMapUv", CLEARCOATROUGHNESSMAP: "vClearcoatRoughnessMapUv", CLEARCOATNORMALMAP: "vClearcoatNormalMapUv", SPECULARINTENSITYMAP: "vSpecularIntensityMapUv", SPECULARCOLORMAP: "vSpecularColorMapUv", TRANSMISSIONMAP: "vTransmissionMapUv", THICKNESSMAP: "vThicknessMapUv", IRIDESCENCEMAP: "vIridescenceMapUv", IRIDESCENCETHICKNESSMAP: "vIridescenceThicknessMapUv" }).map(([role, name]) => {
|
|
70
|
+
return `#ifdef USE_${role}\n ${name} *= surfaceRepeat;\n#endif`;
|
|
71
|
+
}).join("\n")}`);
|
|
72
|
+
};
|
|
73
|
+
material.customProgramCacheKey = () => `${previousKey}|authored-surface:${shape}:${tile}`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Load declared surface-map roles and own the material/map clones (cached textures remain untouched).
|
|
77
|
+
* Colour, emissive, sheen-colour and specular-colour maps are sRGB; numeric maps are linear. Cylinder side metres use the RMS ellipse circumference approximation.
|
|
78
|
+
* Static batches and custom meshes share this lifecycle.
|
|
79
|
+
* @capability authored-surface-material render declared PBR maps with colour-space handling and metre-scale box/cylinder UVs
|
|
80
|
+
*/
|
|
81
|
+
export function useAuthoredSurfaceMaterial(config, shape = "box") {
|
|
82
|
+
const key = authoredSurfaceKey(config);
|
|
83
|
+
const entries = useMemo(() => modelMapEntries(config.maps ?? {}), [key]);
|
|
84
|
+
const loaded = useLoader(THREE.TextureLoader, Object.values(entries));
|
|
85
|
+
const resources = useDisposable(() => {
|
|
86
|
+
const textures = authoredSurfaceTextures(entries, loaded, config);
|
|
87
|
+
const { anisotropy: _samplerAnisotropy, surfaceAnisotropy, ...surface } = config;
|
|
88
|
+
const material = requiresPhysicalMaterial({ ...surface, normalScale: surface.normalScale === undefined ? undefined : [...surface.normalScale], anisotropy: surfaceAnisotropy }, textures) ? new THREE.MeshPhysicalMaterial() : new THREE.MeshStandardMaterial();
|
|
89
|
+
try {
|
|
90
|
+
configureAuthoredSurface(material, config, textures, shape);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
material.dispose();
|
|
94
|
+
Object.values(textures).forEach(texture => texture.dispose());
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
return [material, ...Object.values(textures)];
|
|
98
|
+
}, [key, entries, loaded, shape]);
|
|
99
|
+
return resources[0];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Mount an owned PBR surface material on a mesh.
|
|
103
|
+
* @capability authored-surface-material mount an owned PBR surface material on a mesh
|
|
104
|
+
*/
|
|
105
|
+
export function AuthoredSurfaceMaterial({ surface, shape = "box" }) {
|
|
106
|
+
const material = useAuthoredSurfaceMaterial(surface, shape);
|
|
107
|
+
return _jsx("primitive", { object: material, attach: "material" });
|
|
108
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { type MaterialAppearanceBaseline, type MaterialAppearancePolicy, type MaterialAppearanceResponse } from "@jgengine/core/material/appearanceSignals";
|
|
3
|
+
import type { EnvironmentSample } from "@jgengine/core/world/envField";
|
|
4
|
+
/** Immutable resolved baseline for one caller-owned assigned material, including its original card exposure. */
|
|
5
|
+
export interface RenderMaterialAppearanceBaseline extends MaterialAppearanceBaseline {
|
|
6
|
+
cardBacklightExposure: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Capture once after assignment; recapture after an authored change. Never capture an already wet material.
|
|
10
|
+
* @capability material-appearance-signals capture immutable resolved dry values from one owned assigned material
|
|
11
|
+
*/
|
|
12
|
+
export declare function captureMaterialAppearanceBaseline(material: THREE.MeshStandardMaterial): RenderMaterialAppearanceBaseline;
|
|
13
|
+
/**
|
|
14
|
+
* Apply one sample to an already owned material. Imported/cache materials must be cloned before use.
|
|
15
|
+
* Allocates no textures or materials; clearcoat/sheen require an existing physical material and opt-in policy.
|
|
16
|
+
* @capability material-appearance-signals update one owned material from wetness and local card exposure without global lighting edits
|
|
17
|
+
*/
|
|
18
|
+
export declare function applyMaterialAppearanceSignals(material: THREE.MeshStandardMaterial, baseline: MaterialAppearanceBaseline, sample: EnvironmentSample, policy?: MaterialAppearancePolicy): MaterialAppearanceResponse;
|
|
19
|
+
/**
|
|
20
|
+
* Restore the dry baseline when removing an appearance binding; maps and unrelated imported features remain borrowed.
|
|
21
|
+
* @capability material-appearance-signals restore dry material response when removing an environment appearance binding
|
|
22
|
+
*/
|
|
23
|
+
export declare function restoreMaterialAppearanceBaseline(material: THREE.MeshStandardMaterial, baseline: RenderMaterialAppearanceBaseline): void;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { resolveMaterialAppearance } from "@jgengine/core/material/appearanceSignals";
|
|
3
|
+
import { setHairCardLightExposure } from "./materialAsset.js";
|
|
4
|
+
/**
|
|
5
|
+
* Capture once after assignment; recapture after an authored change. Never capture an already wet material.
|
|
6
|
+
* @capability material-appearance-signals capture immutable resolved dry values from one owned assigned material
|
|
7
|
+
*/
|
|
8
|
+
export function captureMaterialAppearanceBaseline(material) {
|
|
9
|
+
const physical = material;
|
|
10
|
+
return Object.freeze({
|
|
11
|
+
roughness: material.roughness,
|
|
12
|
+
colorLinear: Object.freeze([material.color.r, material.color.g, material.color.b]),
|
|
13
|
+
...(physical.isMeshPhysicalMaterial ? { sheen: physical.sheen, clearcoat: physical.clearcoat, clearcoatRoughness: physical.clearcoatRoughness } : {}),
|
|
14
|
+
cardBacklightExposure: material.userData.jgHairLightExposure ?? 1,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Apply one sample to an already owned material. Imported/cache materials must be cloned before use.
|
|
19
|
+
* Allocates no textures or materials; clearcoat/sheen require an existing physical material and opt-in policy.
|
|
20
|
+
* @capability material-appearance-signals update one owned material from wetness and local card exposure without global lighting edits
|
|
21
|
+
*/
|
|
22
|
+
export function applyMaterialAppearanceSignals(material, baseline, sample, policy = {}) {
|
|
23
|
+
if ((policy.wetness?.sheen !== undefined || policy.wetness?.clearcoat !== undefined) && !material.isMeshPhysicalMaterial)
|
|
24
|
+
throw new TypeError("Wet sheen or clearcoat requires an owned MeshPhysicalMaterial; declare the capability when assigning it.");
|
|
25
|
+
const response = resolveMaterialAppearance(sample, baseline, policy);
|
|
26
|
+
const physical = material;
|
|
27
|
+
if (response.surface.roughness !== undefined)
|
|
28
|
+
material.roughness = response.surface.roughness;
|
|
29
|
+
if (response.colorLinear !== undefined)
|
|
30
|
+
material.color.setRGB(...response.colorLinear, THREE.LinearSRGBColorSpace);
|
|
31
|
+
if (response.surface.sheen !== undefined)
|
|
32
|
+
physical.sheen = response.surface.sheen;
|
|
33
|
+
if (response.surface.clearcoat !== undefined)
|
|
34
|
+
physical.clearcoat = response.surface.clearcoat;
|
|
35
|
+
if (response.surface.clearcoatRoughness !== undefined)
|
|
36
|
+
physical.clearcoatRoughness = response.surface.clearcoatRoughness;
|
|
37
|
+
if (response.backlightExposure !== undefined)
|
|
38
|
+
setHairCardLightExposure(material, response.backlightExposure);
|
|
39
|
+
return response;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Restore the dry baseline when removing an appearance binding; maps and unrelated imported features remain borrowed.
|
|
43
|
+
* @capability material-appearance-signals restore dry material response when removing an environment appearance binding
|
|
44
|
+
*/
|
|
45
|
+
export function restoreMaterialAppearanceBaseline(material, baseline) {
|
|
46
|
+
if ((baseline.sheen !== undefined || baseline.clearcoat !== undefined) && !material.isMeshPhysicalMaterial)
|
|
47
|
+
throw new TypeError("The appearance baseline belongs to a physical material.");
|
|
48
|
+
material.roughness = baseline.roughness;
|
|
49
|
+
material.color.setRGB(...baseline.colorLinear, THREE.LinearSRGBColorSpace);
|
|
50
|
+
const physical = material;
|
|
51
|
+
if (baseline.sheen !== undefined)
|
|
52
|
+
physical.sheen = baseline.sheen;
|
|
53
|
+
if (baseline.clearcoat !== undefined)
|
|
54
|
+
physical.clearcoat = baseline.clearcoat;
|
|
55
|
+
if (baseline.clearcoatRoughness !== undefined)
|
|
56
|
+
physical.clearcoatRoughness = baseline.clearcoatRoughness;
|
|
57
|
+
setHairCardLightExposure(material, baseline.cardBacklightExposure);
|
|
58
|
+
}
|