@jgengine/shell 0.18.1 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +504 -7
- package/dist/GameHost.d.ts +12 -1
- package/dist/GameHost.js +14 -1
- package/dist/GamePlayerShell.js +132 -17
- package/dist/Shell3dPresentation.d.ts +5 -11
- package/dist/Shell3dPresentation.js +73 -11
- package/dist/ShellChrome.d.ts +2 -1
- package/dist/ShellChrome.js +22 -6
- package/dist/ShellHudPresentation.d.ts +20 -2
- package/dist/ShellHudPresentation.js +41 -6
- package/dist/audio/AudioComponents.js +6 -2
- package/dist/audio/audioEngine.d.ts +24 -0
- package/dist/audio/audioEngine.js +231 -83
- package/dist/audio/audioWire.d.ts +3 -0
- package/dist/audio/audioWire.js +9 -3
- package/dist/audio/loopParams.d.ts +39 -0
- package/dist/audio/loopParams.js +43 -0
- package/dist/audio/synthEngine.d.ts +7 -1
- package/dist/audio/synthEngine.js +103 -38
- package/dist/camera/GameCameraRig.js +3 -2
- package/dist/camera/GameFirstPersonCamera.d.ts +6 -4
- package/dist/camera/GameFirstPersonCamera.js +68 -27
- package/dist/camera/Viewports.d.ts +34 -0
- package/dist/camera/Viewports.js +123 -0
- package/dist/camera/cameraRigs.js +76 -62
- package/dist/camera/chaseHeading.d.ts +9 -0
- package/dist/camera/chaseHeading.js +10 -0
- package/dist/camera/customMuzzle.d.ts +14 -0
- package/dist/camera/customMuzzle.js +68 -0
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/orbitCameraMath.js +2 -2
- package/dist/camera/rigMath.d.ts +82 -4
- package/dist/camera/rigMath.js +133 -15
- package/dist/camera/rigResolve.d.ts +3 -0
- package/dist/camera/rigResolve.js +11 -0
- package/dist/combat/ProjectileModels.d.ts +31 -0
- package/dist/combat/ProjectileModels.js +70 -0
- package/dist/commandSink.d.ts +1 -2
- package/dist/commandSink.js +3 -2
- package/dist/defineGame.d.ts +19 -3
- package/dist/defineGame.js +39 -7
- package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
- package/dist/devtools/DevtoolsOverlay.js +15 -167
- package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
- package/dist/devtools/DevtoolsRuntime.js +61 -0
- package/dist/devtools/agentBridge.d.ts +6 -0
- package/dist/devtools/agentBridge.js +16 -2
- package/dist/devtools/devtoolsReports.d.ts +68 -0
- package/dist/devtools/devtoolsReports.js +100 -0
- package/dist/devtools/latencyInstrumentation.d.ts +3 -0
- package/dist/devtools/latencyInstrumentation.js +11 -0
- package/dist/devtools/playtestProbe.d.ts +4 -0
- package/dist/devtools/playtestProbe.js +22 -0
- package/dist/diagnostics/FpsCounter.d.ts +17 -0
- package/dist/diagnostics/FpsCounter.js +32 -0
- package/dist/drivers/FrameDriver.js +35 -20
- package/dist/drivers/FrameRateLimiter.d.ts +9 -0
- package/dist/drivers/FrameRateLimiter.js +39 -0
- package/dist/drivers/HudOnlyDriver.d.ts +6 -1
- package/dist/drivers/HudOnlyDriver.js +32 -14
- package/dist/environment/DayNightSky.js +5 -5
- package/dist/environment/Daylight.d.ts +35 -1
- package/dist/environment/Daylight.js +98 -28
- package/dist/environment/EnvironmentScene.js +11 -10
- package/dist/environment/VolumetricClouds.js +20 -2
- package/dist/environment/daylightCycle.d.ts +20 -0
- package/dist/environment/daylightCycle.js +59 -7
- package/dist/environment/sunShadowMath.d.ts +45 -0
- package/dist/environment/sunShadowMath.js +50 -0
- package/dist/input/gamepadPoll.d.ts +47 -0
- package/dist/input/gamepadPoll.js +142 -0
- package/dist/input/gamepadSource.d.ts +11 -1
- package/dist/input/gamepadSource.js +117 -71
- package/dist/input/mouseLook.js +2 -1
- package/dist/input/padHaptics.d.ts +17 -0
- package/dist/input/padHaptics.js +32 -0
- package/dist/input/pointerLock.d.ts +11 -0
- package/dist/input/pointerLock.js +19 -0
- package/dist/inputSink.d.ts +6 -6
- package/dist/inputSink.js +60 -21
- package/dist/lazyShell3dPresentation.d.ts +9 -0
- package/dist/lazyShell3dPresentation.js +24 -0
- package/dist/materialOverride.d.ts +42 -0
- package/dist/materialOverride.js +95 -36
- package/dist/multiplayer.d.ts +33 -3
- package/dist/multiplayer.js +105 -22
- package/dist/pointer/pointerService.d.ts +2 -0
- package/dist/pointer/pointerService.js +12 -2
- package/dist/postfx/PostProcessing.d.ts +11 -1
- package/dist/postfx/PostProcessing.js +14 -42
- package/dist/postfx/gradeShader.d.ts +1 -1
- package/dist/postfx/gradeShader.js +30 -2
- package/dist/postfx/shadowPrepass.d.ts +3 -0
- package/dist/postfx/shadowPrepass.js +18 -0
- package/dist/postfx/stylizePass.d.ts +23 -0
- package/dist/postfx/stylizePass.js +216 -0
- package/dist/presentationRecovery.d.ts +75 -0
- package/dist/presentationRecovery.js +204 -0
- package/dist/presentationRecovery3d.d.ts +4 -0
- package/dist/presentationRecovery3d.js +32 -0
- package/dist/presentationResolve.d.ts +8 -1
- package/dist/presentationResolve.js +5 -2
- package/dist/render/CascadedShadows.d.ts +7 -2
- package/dist/render/CascadedShadows.js +98 -20
- package/dist/render/EnvironmentLighting.js +74 -35
- package/dist/render/HairCards.d.ts +7 -0
- package/dist/render/HairCards.js +23 -0
- package/dist/render/SceneModels.js +52 -78
- package/dist/render/SkinnedInstances.d.ts +91 -0
- package/dist/render/SkinnedInstances.js +249 -0
- package/dist/render/TextureFiltering.d.ts +16 -0
- package/dist/render/TextureFiltering.js +64 -0
- package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
- package/dist/render/authoredSurfaceMaterial.js +108 -0
- package/dist/render/displayColor.d.ts +33 -0
- package/dist/render/displayColor.js +138 -0
- package/dist/render/materialAppearanceSignals.d.ts +23 -0
- package/dist/render/materialAppearanceSignals.js +58 -0
- package/dist/render/materialAsset.d.ts +98 -0
- package/dist/render/materialAsset.js +454 -0
- package/dist/render/measureBounds.js +16 -3
- package/dist/render/measureCollisionMesh.js +8 -1
- package/dist/render/modelAssets.d.ts +8 -0
- package/dist/render/modelAssets.js +43 -0
- package/dist/render/modelBindPose.d.ts +5 -0
- package/dist/render/modelBindPose.js +35 -0
- package/dist/render/modelLoad.d.ts +32 -0
- package/dist/render/modelLoad.js +70 -1
- package/dist/render/modelNodeVisibility.d.ts +6 -0
- package/dist/render/modelNodeVisibility.js +18 -0
- package/dist/render/modelRender.d.ts +18 -1
- package/dist/render/modelRender.js +81 -3
- package/dist/render/projectedOverdraw.d.ts +35 -0
- package/dist/render/projectedOverdraw.js +204 -0
- package/dist/render/resolveModel.d.ts +12 -2
- package/dist/render/resolveModel.js +25 -4
- package/dist/render/rigNode.d.ts +26 -0
- package/dist/render/rigNode.js +32 -0
- package/dist/render/spriteRender.d.ts +4 -0
- package/dist/render/spriteRender.js +5 -0
- package/dist/render/staticShapeInstances.d.ts +34 -0
- package/dist/render/staticShapeInstances.js +78 -0
- package/dist/render/useFootIk.d.ts +53 -4
- package/dist/render/useFootIk.js +262 -70
- package/dist/render/useModelAnimation.d.ts +50 -0
- package/dist/render/useModelAnimation.js +245 -99
- package/dist/render/useModelInstance.d.ts +25 -0
- package/dist/render/useModelInstance.js +43 -0
- package/dist/scatter/scatterModels.d.ts +3 -2
- package/dist/scatter/scatterModels.js +15 -7
- package/dist/scene/AuthoredScene.d.ts +23 -6
- package/dist/scene/AuthoredScene.js +46 -9
- package/dist/scene/CityRenderer.js +6 -6
- package/dist/scene/GeneratedAssetRenderer.js +11 -42
- package/dist/settings/QuickControls.js +1 -1
- package/dist/settings/SettingsMenu.js +11 -18
- package/dist/settings/SettingsRuntime.d.ts +5 -0
- package/dist/settings/SettingsRuntime.js +29 -2
- package/dist/settings/appliedSettings.d.ts +9 -3
- package/dist/settings/appliedSettings.js +6 -5
- package/dist/settings/settingsController.d.ts +3 -0
- package/dist/settings/settingsController.js +47 -3
- package/dist/shellControlSuspension.d.ts +18 -0
- package/dist/shellControlSuspension.js +28 -0
- package/dist/shellInputPublication.d.ts +28 -0
- package/dist/shellInputPublication.js +49 -0
- package/dist/shellPointerInput.d.ts +18 -0
- package/dist/shellPointerInput.js +49 -0
- package/dist/structures/BuildingKitBatch.d.ts +3 -1
- package/dist/structures/BuildingKitBatch.js +35 -56
- package/dist/structures/BuildingRenderBudget.d.ts +23 -0
- package/dist/structures/BuildingRenderBudget.js +54 -0
- package/dist/structures/GeneratedBuilding.d.ts +3 -15
- package/dist/structures/GeneratedBuilding.js +33 -22
- package/dist/structures/buildingKitMaterials.d.ts +20 -0
- package/dist/structures/buildingKitMaterials.js +70 -0
- package/dist/structures/buildingSpatialBatch.d.ts +11 -0
- package/dist/structures/buildingSpatialBatch.js +45 -0
- package/dist/structures/buildingSurface.d.ts +21 -0
- package/dist/structures/buildingSurface.js +134 -0
- package/dist/terrain/GrassField.d.ts +2 -1
- package/dist/terrain/GrassField.js +59 -5
- package/dist/terrain/grassGeometry.d.ts +10 -0
- package/dist/terrain/grassGeometry.js +55 -0
- package/dist/terrain/grassMaterial.js +2 -2
- package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
- package/dist/terrain/terrainDetailMaterial.js +7 -7
- package/dist/touch/TouchControlsOverlay.js +4 -4
- package/dist/useShellMultiplayerSync.d.ts +1 -1
- package/dist/useShellMultiplayerSync.js +9 -3
- package/dist/vfx/ParticleField.d.ts +23 -20
- package/dist/vfx/ParticleField.js +168 -50
- package/dist/visibility/CullingProvider.js +2 -62
- package/dist/visibility/cullingDriver.d.ts +9 -0
- package/dist/visibility/cullingDriver.js +68 -0
- package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
- package/dist/weather/AuthoredWeatherLayer.js +31 -0
- package/dist/weather/DustField.d.ts +2 -1
- package/dist/weather/DustField.js +17 -3
- package/dist/weather/FireSpreadLayer.d.ts +4 -2
- package/dist/weather/FireSpreadLayer.js +5 -11
- package/dist/weather/LightningStrike.d.ts +4 -1
- package/dist/weather/LightningStrike.js +13 -17
- package/dist/weather/RainField.d.ts +2 -1
- package/dist/weather/RainField.js +17 -3
- package/dist/weather/RainImpactField.d.ts +16 -0
- package/dist/weather/RainImpactField.js +89 -0
- package/dist/weather/SnowField.d.ts +2 -1
- package/dist/weather/SnowField.js +17 -3
- package/dist/weather/WeatherLayer.d.ts +11 -2
- package/dist/weather/WeatherLayer.js +30 -6
- package/dist/weather/index.d.ts +2 -0
- package/dist/weather/index.js +2 -0
- package/dist/weather/weatherMath.d.ts +2 -0
- package/dist/weather/weatherMath.js +8 -0
- package/dist/weather/weatherShelter.d.ts +18 -0
- package/dist/weather/weatherShelter.js +75 -0
- package/dist/weather/weatherUniforms.d.ts +40 -1
- package/dist/weather/weatherUniforms.js +29 -9
- package/dist/world/AuthoredFlocks.d.ts +22 -0
- package/dist/world/AuthoredFlocks.js +87 -0
- package/dist/world/StaticObjectModels.d.ts +8 -0
- package/dist/world/StaticObjectModels.js +75 -0
- package/dist/world/WorldHud.d.ts +14 -6
- package/dist/world/WorldHud.js +25 -12
- package/dist/world/WorldParticles.d.ts +10 -23
- package/dist/world/WorldParticles.js +210 -62
- package/dist/world/WorldScene.d.ts +3 -1
- package/dist/world/WorldScene.js +41 -26
- package/dist/world/staticObjectBatches.d.ts +35 -0
- package/dist/world/staticObjectBatches.js +163 -0
- package/dist/world/worldBarSamples.d.ts +37 -33
- package/dist/world/worldBarSamples.js +117 -75
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +2 -2
- package/skills/game-design/SKILL.md +2 -2
- package/skills/jgengine/SKILL.md +28 -6
- package/skills/jgengine/api.md +139 -99
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine/recipes/minimal-game.md +10 -2
- package/skills/jgengine-ui/SKILL.md +16 -17
- package/skills/jgengine-ui/api.md +603 -365
- package/skills/jgengine-ui/capabilities.md +131 -1
- package/skills/jgengine-ui/reference-primitives.md +179 -0
- package/skills/jgengine-ui/reference.md +4 -81
- package/skills/jgengine-world/SKILL.md +25 -4
- package/skills/jgengine-world/api.md +1045 -655
- package/skills/jgengine-world/capabilities.md +246 -9
- package/skills/jgengine-world/recipes/character-animation.md +62 -0
- package/skills/jgengine-world/recipes/controls.md +48 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +55 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
- package/skills/jgengine-world/reference.md +180 -4
- package/skills/jgengine-world/visibility.md +139 -0
- package/skills/level-design/SKILL.md +1 -1
- package/dist/render/testFixtures/proceduralSpriteAtlas.d.ts +0 -15
- package/dist/render/testFixtures/proceduralSpriteAtlas.js +0 -30
- package/dist/render/testFixtures/proceduralTileLayer.d.ts +0 -13
- package/dist/render/testFixtures/proceduralTileLayer.js +0 -30
|
@@ -1,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,8 +192,7 @@ 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
|
-
|
|
221
|
-
useFootIk(scene, model.ik, ctx, instanceId);
|
|
195
|
+
useFootIk(scene, model.ik, ctx, instanceId, baseY);
|
|
222
196
|
const paintCanvasRef = useRef(null);
|
|
223
197
|
const paintDrawnCountRef = useRef(0);
|
|
224
198
|
const paintVersionRef = useRef(-1);
|
|
@@ -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,91 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { type BoneTextureLayout } from "@jgengine/core/anim/boneTexture";
|
|
3
|
+
/** A rig to bake: a loaded scene with skinned meshes and its clips. */
|
|
4
|
+
export interface SkinnedCrowdSource {
|
|
5
|
+
scene: THREE.Object3D;
|
|
6
|
+
animations: readonly THREE.AnimationClip[];
|
|
7
|
+
}
|
|
8
|
+
/** Options for {@link bakeSkinnedCrowd}. */
|
|
9
|
+
export interface BakeSkinnedCrowdOptions {
|
|
10
|
+
/** Clip names to bake, in order; default every clip on the rig. */
|
|
11
|
+
clips?: readonly string[];
|
|
12
|
+
/** Baked samples per second. Default 30. */
|
|
13
|
+
fps?: number;
|
|
14
|
+
/** Clips that hold their last pose instead of looping. */
|
|
15
|
+
once?: readonly string[];
|
|
16
|
+
maxTextureSize?: number;
|
|
17
|
+
}
|
|
18
|
+
/** A rig baked for instancing: one merged geometry, the bone-matrix texture and its layout. */
|
|
19
|
+
export interface BakedSkinnedCrowd {
|
|
20
|
+
geometry: THREE.BufferGeometry;
|
|
21
|
+
/** One material per merged mesh, in geometry-group order, not yet patched. */
|
|
22
|
+
materials: THREE.Material[];
|
|
23
|
+
texture: THREE.DataTexture;
|
|
24
|
+
layout: BoneTextureLayout;
|
|
25
|
+
/** Per clip: whether it loops. Same order as `layout.clips`. */
|
|
26
|
+
loops: boolean[];
|
|
27
|
+
clipIndex: ReadonlyMap<string, number>;
|
|
28
|
+
dispose(): void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Bakes a rig for {@link SkinnedInstances}: merges its skinned meshes into one geometry in bind
|
|
32
|
+
* space, samples every requested clip at `fps`, and writes each bone's model-space skinning matrix
|
|
33
|
+
* into a float texture laid out by `planBoneTexture`. Meshes with several materials keep only the
|
|
34
|
+
* first. Exported for tests and custom crowd renderers.
|
|
35
|
+
*/
|
|
36
|
+
export declare function bakeSkinnedCrowd(source: SkinnedCrowdSource, options?: BakeSkinnedCrowdOptions): BakedSkinnedCrowd;
|
|
37
|
+
/** Uniforms shared by every material of one crowd; `crowdTime` advances once per frame. */
|
|
38
|
+
export interface CrowdUniforms {
|
|
39
|
+
crowdBones: {
|
|
40
|
+
value: THREE.DataTexture;
|
|
41
|
+
};
|
|
42
|
+
crowdTime: {
|
|
43
|
+
value: number;
|
|
44
|
+
};
|
|
45
|
+
crowdClips: {
|
|
46
|
+
value: THREE.Vector4[];
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Builds the uniforms a crowd material reads: the texture and per-clip start, frames, fps and loop. */
|
|
50
|
+
export declare function createCrowdUniforms(baked: BakedSkinnedCrowd): CrowdUniforms;
|
|
51
|
+
/**
|
|
52
|
+
* Patches a material so each instance skins itself from the crowd texture: per-instance `crowdPlay`
|
|
53
|
+
* is (clip index, time offset, speed). Works on any built-in material, the depth material for
|
|
54
|
+
* shadows included.
|
|
55
|
+
*/
|
|
56
|
+
export declare function patchCrowdMaterial<T extends THREE.Material>(material: T, uniforms: CrowdUniforms): T;
|
|
57
|
+
/** One crowd member: where it stands and what it plays. */
|
|
58
|
+
export interface CrowdInstance {
|
|
59
|
+
position: readonly [number, number, number];
|
|
60
|
+
rotationY?: number;
|
|
61
|
+
scale?: number;
|
|
62
|
+
/** A baked clip name. */
|
|
63
|
+
clip: string;
|
|
64
|
+
/** Seconds into the clip at crowd time zero, so members do not move in lockstep. */
|
|
65
|
+
timeOffset?: number;
|
|
66
|
+
/** Playback rate. Default 1. */
|
|
67
|
+
speed?: number;
|
|
68
|
+
}
|
|
69
|
+
/** Props for {@link SkinnedInstances}. */
|
|
70
|
+
export interface SkinnedInstancesProps {
|
|
71
|
+
url: string;
|
|
72
|
+
instances: readonly CrowdInstance[];
|
|
73
|
+
/** Clip names to bake; default every clip on the rig. */
|
|
74
|
+
clips?: readonly string[];
|
|
75
|
+
/** Clips that hold their last pose instead of looping. */
|
|
76
|
+
once?: readonly string[];
|
|
77
|
+
fps?: number;
|
|
78
|
+
/** Uniform scale applied to every member, e.g. to normalize a rig to a height. */
|
|
79
|
+
modelScale?: number;
|
|
80
|
+
castShadow?: boolean;
|
|
81
|
+
receiveShadow?: boolean;
|
|
82
|
+
/** Multiplies crowd time; `0` freezes every member. Default 1. */
|
|
83
|
+
timeScale?: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Renders many animated copies of one rigged model in a single instanced draw per material: the
|
|
87
|
+
* rig's clips are baked to a bone-matrix texture once, and each instance samples its own clip, time
|
|
88
|
+
* offset and speed on the GPU. For crowds, spectators and distant NPCs; a character that needs
|
|
89
|
+
* blending, IK or attachments stays an entity model.
|
|
90
|
+
*/
|
|
91
|
+
export declare function SkinnedInstances({ url, instances, clips, once, fps, modelScale, castShadow, receiveShadow, timeScale, }: SkinnedInstancesProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame, useLoader } from "@react-three/fiber";
|
|
3
|
+
import { useEffect, useLayoutEffect, useMemo } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";
|
|
6
|
+
import { clone as cloneSkinned } from "three/examples/jsm/utils/SkeletonUtils.js";
|
|
7
|
+
import { planBoneTexture } from "@jgengine/core/anim/boneTexture";
|
|
8
|
+
import { sharedGltfLoader } from "./modelLoad.js";
|
|
9
|
+
function slotFor(slots, bone, inverse) {
|
|
10
|
+
const found = slots.findIndex((slot) => slot.bone === bone && slot.inverse.equals(inverse));
|
|
11
|
+
if (found >= 0)
|
|
12
|
+
return found;
|
|
13
|
+
slots.push({ bone, inverse: inverse.clone() });
|
|
14
|
+
return slots.length - 1;
|
|
15
|
+
}
|
|
16
|
+
function firstMaterial(material) {
|
|
17
|
+
return Array.isArray(material) ? material[0] : material;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Bakes a rig for {@link SkinnedInstances}: merges its skinned meshes into one geometry in bind
|
|
21
|
+
* space, samples every requested clip at `fps`, and writes each bone's model-space skinning matrix
|
|
22
|
+
* into a float texture laid out by `planBoneTexture`. Meshes with several materials keep only the
|
|
23
|
+
* first. Exported for tests and custom crowd renderers.
|
|
24
|
+
*/
|
|
25
|
+
export function bakeSkinnedCrowd(source, options = {}) {
|
|
26
|
+
const root = cloneSkinned(source.scene);
|
|
27
|
+
root.position.set(0, 0, 0);
|
|
28
|
+
root.quaternion.identity();
|
|
29
|
+
root.scale.set(1, 1, 1);
|
|
30
|
+
root.updateMatrixWorld(true);
|
|
31
|
+
const meshes = [];
|
|
32
|
+
root.traverse((object) => {
|
|
33
|
+
if (object.isSkinnedMesh === true)
|
|
34
|
+
meshes.push(object);
|
|
35
|
+
});
|
|
36
|
+
if (meshes.length === 0)
|
|
37
|
+
throw new Error("bakeSkinnedCrowd: the source has no skinned mesh");
|
|
38
|
+
const slots = [];
|
|
39
|
+
const geometries = [];
|
|
40
|
+
const materials = [];
|
|
41
|
+
for (const mesh of meshes) {
|
|
42
|
+
const source = mesh.geometry;
|
|
43
|
+
const geometry = new THREE.BufferGeometry();
|
|
44
|
+
geometry.setAttribute("position", source.getAttribute("position").clone());
|
|
45
|
+
if (source.getAttribute("normal") !== undefined)
|
|
46
|
+
geometry.setAttribute("normal", source.getAttribute("normal").clone());
|
|
47
|
+
else
|
|
48
|
+
geometry.setAttribute("normal", new THREE.Float32BufferAttribute(new Float32Array(source.getAttribute("position").count * 3), 3));
|
|
49
|
+
const uv = source.getAttribute("uv");
|
|
50
|
+
geometry.setAttribute("uv", uv !== undefined ? uv.clone() : new THREE.Float32BufferAttribute(new Float32Array(source.getAttribute("position").count * 2), 2));
|
|
51
|
+
if (source.index !== null)
|
|
52
|
+
geometry.setIndex(source.index.clone());
|
|
53
|
+
geometry.applyMatrix4(mesh.bindMatrix);
|
|
54
|
+
const skinIndex = source.getAttribute("skinIndex");
|
|
55
|
+
const skinWeight = source.getAttribute("skinWeight");
|
|
56
|
+
const remapped = new Float32Array(skinIndex.count * 4);
|
|
57
|
+
const weights = new Float32Array(skinWeight.count * 4);
|
|
58
|
+
const bySkeletonIndex = mesh.skeleton.bones.map((bone, i) => slotFor(slots, bone, mesh.skeleton.boneInverses[i]));
|
|
59
|
+
for (let v = 0; v < skinIndex.count; v += 1) {
|
|
60
|
+
for (let c = 0; c < 4; c += 1) {
|
|
61
|
+
remapped[v * 4 + c] = bySkeletonIndex[skinIndex.getComponent(v, c)] ?? 0;
|
|
62
|
+
weights[v * 4 + c] = skinWeight.getComponent(v, c);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
geometry.setAttribute("skinIndex", new THREE.Float32BufferAttribute(remapped, 4));
|
|
66
|
+
geometry.setAttribute("skinWeight", new THREE.Float32BufferAttribute(weights, 4));
|
|
67
|
+
geometries.push(geometry);
|
|
68
|
+
materials.push(firstMaterial(mesh.material));
|
|
69
|
+
}
|
|
70
|
+
const geometry = mergeGeometries(geometries, true);
|
|
71
|
+
for (const part of geometries)
|
|
72
|
+
part.dispose();
|
|
73
|
+
if (geometry === null)
|
|
74
|
+
throw new Error("bakeSkinnedCrowd: could not merge the rig's meshes");
|
|
75
|
+
geometry.computeBoundingBox();
|
|
76
|
+
geometry.computeBoundingSphere();
|
|
77
|
+
const names = options.clips ?? source.animations.map((clip) => clip.name);
|
|
78
|
+
const clips = names.map((name) => {
|
|
79
|
+
const clip = THREE.AnimationClip.findByName(source.animations, name);
|
|
80
|
+
if (clip === null)
|
|
81
|
+
throw new Error(`bakeSkinnedCrowd: no clip named "${name}"`);
|
|
82
|
+
return clip;
|
|
83
|
+
});
|
|
84
|
+
const layout = planBoneTexture({
|
|
85
|
+
boneCount: slots.length,
|
|
86
|
+
clips: clips.map((clip) => ({ name: clip.name, duration: clip.duration })),
|
|
87
|
+
fps: options.fps,
|
|
88
|
+
maxTextureSize: options.maxTextureSize,
|
|
89
|
+
});
|
|
90
|
+
const data = new Float32Array(layout.width * layout.height * 4);
|
|
91
|
+
const mixer = new THREE.AnimationMixer(root);
|
|
92
|
+
const rootInverse = root.matrixWorld.clone().invert();
|
|
93
|
+
const matrix = new THREE.Matrix4();
|
|
94
|
+
clips.forEach((clip, clipIndex) => {
|
|
95
|
+
const baked = layout.clips[clipIndex];
|
|
96
|
+
const action = mixer.clipAction(clip);
|
|
97
|
+
action.play();
|
|
98
|
+
for (let frame = 0; frame < baked.frameCount; frame += 1) {
|
|
99
|
+
action.time = Math.min(frame / layout.fps, clip.duration);
|
|
100
|
+
mixer.update(0);
|
|
101
|
+
root.updateMatrixWorld(true);
|
|
102
|
+
const rowOffset = (baked.startFrame + frame) * layout.width * 4;
|
|
103
|
+
slots.forEach((slot, s) => {
|
|
104
|
+
matrix.multiplyMatrices(rootInverse, slot.bone.matrixWorld).multiply(slot.inverse);
|
|
105
|
+
data.set(matrix.elements, rowOffset + s * 16);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
action.stop();
|
|
109
|
+
});
|
|
110
|
+
mixer.uncacheRoot(root);
|
|
111
|
+
const texture = new THREE.DataTexture(data, layout.width, layout.height, THREE.RGBAFormat, THREE.FloatType);
|
|
112
|
+
texture.minFilter = THREE.NearestFilter;
|
|
113
|
+
texture.magFilter = THREE.NearestFilter;
|
|
114
|
+
texture.generateMipmaps = false;
|
|
115
|
+
texture.needsUpdate = true;
|
|
116
|
+
const once = new Set(options.once ?? []);
|
|
117
|
+
return {
|
|
118
|
+
geometry,
|
|
119
|
+
materials,
|
|
120
|
+
texture,
|
|
121
|
+
layout,
|
|
122
|
+
loops: layout.clips.map((clip) => !once.has(clip.name)),
|
|
123
|
+
clipIndex: new Map(layout.clips.map((clip, index) => [clip.name, index])),
|
|
124
|
+
dispose() {
|
|
125
|
+
geometry.dispose();
|
|
126
|
+
texture.dispose();
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** Builds the uniforms a crowd material reads: the texture and per-clip start, frames, fps and loop. */
|
|
131
|
+
export function createCrowdUniforms(baked) {
|
|
132
|
+
return {
|
|
133
|
+
crowdBones: { value: baked.texture },
|
|
134
|
+
crowdTime: { value: 0 },
|
|
135
|
+
crowdClips: {
|
|
136
|
+
value: baked.layout.clips.map((clip, index) => new THREE.Vector4(clip.startFrame, clip.frameCount, baked.layout.fps, baked.loops[index] ? 1 : 0)),
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const CROWD_HEADER = /* glsl */ `
|
|
141
|
+
uniform highp sampler2D crowdBones;
|
|
142
|
+
uniform float crowdTime;
|
|
143
|
+
uniform vec4 crowdClips[CROWD_CLIP_COUNT];
|
|
144
|
+
attribute vec4 skinIndex;
|
|
145
|
+
attribute vec4 skinWeight;
|
|
146
|
+
attribute vec3 crowdPlay;
|
|
147
|
+
mat4 crowdMatrix;
|
|
148
|
+
bool crowdReady = false;
|
|
149
|
+
mat4 crowdBone(float slot, float row) {
|
|
150
|
+
int x = int(slot + 0.5) * 4;
|
|
151
|
+
int y = int(row + 0.5);
|
|
152
|
+
return mat4(
|
|
153
|
+
texelFetch(crowdBones, ivec2(x, y), 0),
|
|
154
|
+
texelFetch(crowdBones, ivec2(x + 1, y), 0),
|
|
155
|
+
texelFetch(crowdBones, ivec2(x + 2, y), 0),
|
|
156
|
+
texelFetch(crowdBones, ivec2(x + 3, y), 0));
|
|
157
|
+
}
|
|
158
|
+
mat4 crowdSkin() {
|
|
159
|
+
if (crowdReady) return crowdMatrix;
|
|
160
|
+
vec4 clip = crowdClips[int(crowdPlay.x + 0.5)];
|
|
161
|
+
float span = clip.y - 1.0;
|
|
162
|
+
float raw = (crowdTime * crowdPlay.z + crowdPlay.y) * clip.z;
|
|
163
|
+
float frame = clip.w > 0.5 ? mod(raw, span) : clamp(raw, 0.0, span);
|
|
164
|
+
float a = floor(frame);
|
|
165
|
+
float t = frame - a;
|
|
166
|
+
float rowA = clip.x + a;
|
|
167
|
+
float rowB = clip.x + min(a + 1.0, span);
|
|
168
|
+
crowdMatrix = mat4(0.0);
|
|
169
|
+
for (int i = 0; i < 4; i++) {
|
|
170
|
+
float w = skinWeight[i];
|
|
171
|
+
if (w > 0.0) crowdMatrix += w * ((1.0 - t) * crowdBone(skinIndex[i], rowA) + t * crowdBone(skinIndex[i], rowB));
|
|
172
|
+
}
|
|
173
|
+
crowdReady = true;
|
|
174
|
+
return crowdMatrix;
|
|
175
|
+
}
|
|
176
|
+
`;
|
|
177
|
+
/**
|
|
178
|
+
* Patches a material so each instance skins itself from the crowd texture: per-instance `crowdPlay`
|
|
179
|
+
* is (clip index, time offset, speed). Works on any built-in material, the depth material for
|
|
180
|
+
* shadows included.
|
|
181
|
+
*/
|
|
182
|
+
export function patchCrowdMaterial(material, uniforms) {
|
|
183
|
+
const clipCount = Math.max(1, uniforms.crowdClips.value.length);
|
|
184
|
+
material.onBeforeCompile = (shader) => {
|
|
185
|
+
Object.assign(shader.uniforms, uniforms);
|
|
186
|
+
shader.vertexShader = shader.vertexShader
|
|
187
|
+
.replace("#include <common>", `#include <common>\n#define CROWD_CLIP_COUNT ${clipCount}\n${CROWD_HEADER}`)
|
|
188
|
+
.replace("#include <beginnormal_vertex>", "#include <beginnormal_vertex>\nobjectNormal = mat3(crowdSkin()) * objectNormal;")
|
|
189
|
+
.replace("#include <begin_vertex>", "#include <begin_vertex>\ntransformed = (crowdSkin() * vec4(transformed, 1.0)).xyz;");
|
|
190
|
+
};
|
|
191
|
+
material.customProgramCacheKey = () => `jg-crowd-${clipCount}`;
|
|
192
|
+
return material;
|
|
193
|
+
}
|
|
194
|
+
const scratchMatrix = new THREE.Matrix4();
|
|
195
|
+
const scratchQuaternion = new THREE.Quaternion();
|
|
196
|
+
const scratchPosition = new THREE.Vector3();
|
|
197
|
+
const scratchScale = new THREE.Vector3();
|
|
198
|
+
/**
|
|
199
|
+
* Renders many animated copies of one rigged model in a single instanced draw per material: the
|
|
200
|
+
* rig's clips are baked to a bone-matrix texture once, and each instance samples its own clip, time
|
|
201
|
+
* offset and speed on the GPU. For crowds, spectators and distant NPCs; a character that needs
|
|
202
|
+
* blending, IK or attachments stays an entity model.
|
|
203
|
+
*/
|
|
204
|
+
export function SkinnedInstances({ url, instances, clips, once, fps, modelScale = 1, castShadow = true, receiveShadow = true, timeScale = 1, }) {
|
|
205
|
+
const gltf = useLoader(sharedGltfLoader, url);
|
|
206
|
+
const clipKey = clips?.join("\u0000");
|
|
207
|
+
const onceKey = once?.join("\u0000");
|
|
208
|
+
const baked = useMemo(() => bakeSkinnedCrowd({ scene: gltf.scene, animations: gltf.animations }, { clips, once, fps }), [gltf, clipKey, onceKey, fps]);
|
|
209
|
+
useEffect(() => () => baked.dispose(), [baked]);
|
|
210
|
+
const uniforms = useMemo(() => createCrowdUniforms(baked), [baked]);
|
|
211
|
+
const materials = useMemo(() => baked.materials.map((material) => patchCrowdMaterial(material.clone(), uniforms)), [baked, uniforms]);
|
|
212
|
+
const depthMaterial = useMemo(() => patchCrowdMaterial(new THREE.MeshDepthMaterial({ depthPacking: THREE.RGBADepthPacking }), uniforms), [uniforms]);
|
|
213
|
+
useEffect(() => () => {
|
|
214
|
+
for (const material of materials)
|
|
215
|
+
material.dispose();
|
|
216
|
+
depthMaterial.dispose();
|
|
217
|
+
}, [materials, depthMaterial]);
|
|
218
|
+
const capacity = Math.max(1, instances.length);
|
|
219
|
+
const mesh = useMemo(() => {
|
|
220
|
+
const instanced = new THREE.InstancedMesh(baked.geometry, materials, capacity);
|
|
221
|
+
instanced.customDepthMaterial = depthMaterial;
|
|
222
|
+
instanced.geometry.setAttribute("crowdPlay", new THREE.InstancedBufferAttribute(new Float32Array(capacity * 3), 3));
|
|
223
|
+
return instanced;
|
|
224
|
+
}, [baked, materials, depthMaterial, capacity]);
|
|
225
|
+
useEffect(() => () => {
|
|
226
|
+
mesh.dispose();
|
|
227
|
+
}, [mesh]);
|
|
228
|
+
useLayoutEffect(() => {
|
|
229
|
+
const play = mesh.geometry.getAttribute("crowdPlay");
|
|
230
|
+
instances.forEach((instance, i) => {
|
|
231
|
+
const scale = (instance.scale ?? 1) * modelScale;
|
|
232
|
+
scratchQuaternion.setFromAxisAngle(THREE.Object3D.DEFAULT_UP, instance.rotationY ?? 0);
|
|
233
|
+
scratchMatrix.compose(scratchPosition.set(instance.position[0], instance.position[1], instance.position[2]), scratchQuaternion, scratchScale.set(scale, scale, scale));
|
|
234
|
+
mesh.setMatrixAt(i, scratchMatrix);
|
|
235
|
+
play.setXYZ(i, baked.clipIndex.get(instance.clip) ?? 0, instance.timeOffset ?? 0, instance.speed ?? 1);
|
|
236
|
+
});
|
|
237
|
+
mesh.count = instances.length;
|
|
238
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
239
|
+
play.needsUpdate = true;
|
|
240
|
+
mesh.computeBoundingSphere();
|
|
241
|
+
// Poses reach past the bind-pose bounds (arms out, a lunge), so pad the cull sphere.
|
|
242
|
+
if (mesh.boundingSphere !== null)
|
|
243
|
+
mesh.boundingSphere.radius += (baked.geometry.boundingSphere?.radius ?? 0) * modelScale;
|
|
244
|
+
}, [mesh, instances, baked, modelScale]);
|
|
245
|
+
useFrame((_three, delta) => {
|
|
246
|
+
uniforms.crowdTime.value += delta * timeScale;
|
|
247
|
+
});
|
|
248
|
+
return _jsx("primitive", { object: mesh, castShadow: castShadow, receiveShadow: receiveShadow });
|
|
249
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
/**
|
|
3
|
+
* Retune already-loaded textures that still carry the old default anisotropy. Textures a
|
|
4
|
+
* system set on purpose (pixel-art sprites pin 1) keep their value.
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export declare function retuneTextureAnisotropy(root: THREE.Object3D, from: number, to: number): number;
|
|
8
|
+
/**
|
|
9
|
+
* Applies the player's texture-filtering choice. Sets three's construction-time default so
|
|
10
|
+
* every texture loaded afterwards (GLTF, texture loaders) samples anisotropically, and walks the
|
|
11
|
+
* scene once per change for textures loaded before. No per-frame work.
|
|
12
|
+
* @internal shell-internal; driven by `GraphicsProfile.anisotropy`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function TextureFiltering({ anisotropy }: {
|
|
15
|
+
anisotropy: number;
|
|
16
|
+
}): null;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useThree } from "@react-three/fiber";
|
|
2
|
+
import { useLayoutEffect } from "react";
|
|
3
|
+
import * as THREE from "three";
|
|
4
|
+
const TEXTURE_SLOTS = [
|
|
5
|
+
"map",
|
|
6
|
+
"normalMap",
|
|
7
|
+
"roughnessMap",
|
|
8
|
+
"metalnessMap",
|
|
9
|
+
"aoMap",
|
|
10
|
+
"emissiveMap",
|
|
11
|
+
"bumpMap",
|
|
12
|
+
"alphaMap",
|
|
13
|
+
"displacementMap",
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Retune already-loaded textures that still carry the old default anisotropy. Textures a
|
|
17
|
+
* system set on purpose (pixel-art sprites pin 1) keep their value.
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export function retuneTextureAnisotropy(root, from, to) {
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
root.traverse((node) => {
|
|
23
|
+
const mesh = node;
|
|
24
|
+
if (mesh.material === undefined)
|
|
25
|
+
return;
|
|
26
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
27
|
+
for (const material of materials) {
|
|
28
|
+
const slots = material;
|
|
29
|
+
for (const slot of TEXTURE_SLOTS) {
|
|
30
|
+
const texture = slots[slot];
|
|
31
|
+
if (texture === null || texture === undefined || seen.has(texture))
|
|
32
|
+
continue;
|
|
33
|
+
seen.add(texture);
|
|
34
|
+
if (texture.anisotropy !== from)
|
|
35
|
+
continue;
|
|
36
|
+
texture.anisotropy = to;
|
|
37
|
+
texture.needsUpdate = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
return seen.size;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Applies the player's texture-filtering choice. Sets three's construction-time default so
|
|
45
|
+
* every texture loaded afterwards (GLTF, texture loaders) samples anisotropically, and walks the
|
|
46
|
+
* scene once per change for textures loaded before. No per-frame work.
|
|
47
|
+
* @internal shell-internal; driven by `GraphicsProfile.anisotropy`.
|
|
48
|
+
*/
|
|
49
|
+
export function TextureFiltering({ anisotropy }) {
|
|
50
|
+
const gl = useThree((state) => state.gl);
|
|
51
|
+
const scene = useThree((state) => state.scene);
|
|
52
|
+
useLayoutEffect(() => {
|
|
53
|
+
const target = Math.max(1, Math.min(anisotropy, gl.capabilities.getMaxAnisotropy()));
|
|
54
|
+
const previous = THREE.Texture.DEFAULT_ANISOTROPY;
|
|
55
|
+
if (previous === target)
|
|
56
|
+
return;
|
|
57
|
+
THREE.Texture.DEFAULT_ANISOTROPY = target;
|
|
58
|
+
retuneTextureAnisotropy(scene, previous, target);
|
|
59
|
+
return () => {
|
|
60
|
+
THREE.Texture.DEFAULT_ANISOTROPY = previous;
|
|
61
|
+
};
|
|
62
|
+
}, [gl, scene, anisotropy]);
|
|
63
|
+
return null;
|
|
64
|
+
}
|