@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
|
@@ -3,7 +3,7 @@ import { createContext, useContext, useMemo, useRef } from "react";
|
|
|
3
3
|
import { useThree, useFrame } from "@react-three/fiber";
|
|
4
4
|
import * as THREE from "three";
|
|
5
5
|
import { useGameContext } from "@jgengine/react/provider";
|
|
6
|
-
import {
|
|
6
|
+
import { buildCullingDriver } from "./cullingDriver.js";
|
|
7
7
|
import { CAMERA_POST_FRAME_PRIORITY } from "../camera/cameraRigs.js";
|
|
8
8
|
const ALWAYS_VISIBLE = () => true;
|
|
9
9
|
const alwaysVisibleRef = { current: ALWAYS_VISIBLE };
|
|
@@ -16,66 +16,6 @@ const CullingContext = createContext(null);
|
|
|
16
16
|
export function useRenderVisibility() {
|
|
17
17
|
return useContext(CullingContext) ?? alwaysVisibleRef;
|
|
18
18
|
}
|
|
19
|
-
const ENTITY_BOUNDS = { kind: "sphere", radius: 2, offset: [0, 1, 0] };
|
|
20
|
-
function objectRadius(scale) {
|
|
21
|
-
if (scale === undefined)
|
|
22
|
-
return 1;
|
|
23
|
-
const s = typeof scale === "number" ? scale : Math.max(scale[0], scale[1], scale[2]);
|
|
24
|
-
return Math.max(0.75, s);
|
|
25
|
-
}
|
|
26
|
-
function buildSystem(ctx, config) {
|
|
27
|
-
const versions = new Map();
|
|
28
|
-
let currentView = null;
|
|
29
|
-
const scratch = { id: "", position: [0, 0, 0], version: 0 };
|
|
30
|
-
const objectSpec = { kind: "sphere", radius: 1, offset: [0, 0.5, 0] };
|
|
31
|
-
function bump(id, position, rot) {
|
|
32
|
-
const prev = versions.get(id);
|
|
33
|
-
if (prev !== undefined && prev.x === position[0] && prev.y === position[1] && prev.z === position[2] && prev.rot === rot) {
|
|
34
|
-
return prev.version;
|
|
35
|
-
}
|
|
36
|
-
const version = (prev?.version ?? 0) + 1;
|
|
37
|
-
versions.set(id, { x: position[0], y: position[1], z: position[2], rot, version });
|
|
38
|
-
return version;
|
|
39
|
-
}
|
|
40
|
-
function* renderables() {
|
|
41
|
-
const entityOverrides = config?.entities;
|
|
42
|
-
for (const entity of ctx.scene.entity.list()) {
|
|
43
|
-
scratch.id = entity.id;
|
|
44
|
-
scratch.position = entity.position;
|
|
45
|
-
scratch.version = bump(entity.id, entity.position, entity.rotationY);
|
|
46
|
-
scratch.bounds = ENTITY_BOUNDS;
|
|
47
|
-
scratch.overrides = entityOverrides?.[entity.name];
|
|
48
|
-
scratch.layer = "entity";
|
|
49
|
-
yield scratch;
|
|
50
|
-
}
|
|
51
|
-
const objectOverrides = config?.objects;
|
|
52
|
-
for (const object of ctx.scene.object.list()) {
|
|
53
|
-
scratch.id = object.instanceId;
|
|
54
|
-
scratch.position = object.position;
|
|
55
|
-
scratch.version = bump(object.instanceId, object.position, object.rotationY);
|
|
56
|
-
objectSpec.radius = objectRadius(object.visual?.scale);
|
|
57
|
-
scratch.bounds = objectSpec;
|
|
58
|
-
scratch.overrides = objectOverrides?.[object.catalogId];
|
|
59
|
-
scratch.layer = "object";
|
|
60
|
-
yield scratch;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
const cameras = () => (currentView === null ? EMPTY_CAMERAS : [{ id: "main", view: currentView }]);
|
|
64
|
-
const options = {
|
|
65
|
-
renderables,
|
|
66
|
-
cameras,
|
|
67
|
-
...(config?.culling !== undefined ? { settings: config.culling } : {}),
|
|
68
|
-
...(config?.scene !== undefined ? { sceneOverrides: config.scene } : {}),
|
|
69
|
-
};
|
|
70
|
-
const system = createVisibilitySystem(options);
|
|
71
|
-
return {
|
|
72
|
-
system,
|
|
73
|
-
setView(view) {
|
|
74
|
-
currentView = view;
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
const EMPTY_CAMERAS = [];
|
|
79
19
|
const tmpDir = new THREE.Vector3();
|
|
80
20
|
function viewFromCamera(camera) {
|
|
81
21
|
camera.getWorldDirection(tmpDir);
|
|
@@ -120,7 +60,7 @@ export function CullingProvider({ config, drawDistance, children }) {
|
|
|
120
60
|
return config;
|
|
121
61
|
return { ...config, culling: { ...config?.culling, defaultMaxRenderDistance: config?.culling?.defaultMaxRenderDistance ?? drawDistance } };
|
|
122
62
|
}, [config, drawDistance]);
|
|
123
|
-
const driver = useMemo(() => (enabled ?
|
|
63
|
+
const driver = useMemo(() => (enabled ? buildCullingDriver(ctx, resolvedConfig) : null), [ctx, resolvedConfig, enabled]);
|
|
124
64
|
useFrame(() => {
|
|
125
65
|
if (driver === null) {
|
|
126
66
|
predicateRef.current = ALWAYS_VISIBLE;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
2
|
+
import type { CameraView } from "@jgengine/core/visibility/frustum";
|
|
3
|
+
import type { VisibilityConfig } from "@jgengine/core/visibility/config";
|
|
4
|
+
/** Builds the shell's live scene adapter for headless visibility. @internal */
|
|
5
|
+
export declare function buildCullingDriver(ctx: GameContext, config: VisibilityConfig | undefined): {
|
|
6
|
+
system: import("@jgengine/core/visibility/visibilitySystem").VisibilitySystem;
|
|
7
|
+
setView(view: CameraView | null): void;
|
|
8
|
+
trackedCount(): number;
|
|
9
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createVisibilitySystem } from "@jgengine/core/visibility/visibilitySystem";
|
|
2
|
+
const ENTITY_BOUNDS = { kind: "sphere", radius: 2, offset: [0, 1, 0] };
|
|
3
|
+
const EMPTY_CAMERAS = [];
|
|
4
|
+
function objectRadius(scale) {
|
|
5
|
+
if (scale === undefined)
|
|
6
|
+
return 1;
|
|
7
|
+
const size = typeof scale === "number" ? Math.abs(scale) : Math.max(Math.abs(scale[0]), Math.abs(scale[1]), Math.abs(scale[2]));
|
|
8
|
+
return Math.max(0.75, size);
|
|
9
|
+
}
|
|
10
|
+
/** Builds the shell's live scene adapter for headless visibility. @internal */
|
|
11
|
+
export function buildCullingDriver(ctx, config) {
|
|
12
|
+
const entries = new Map();
|
|
13
|
+
let frame = 0;
|
|
14
|
+
let currentView = null;
|
|
15
|
+
function update(id, position, rotation, radius, overrides, layer) {
|
|
16
|
+
let entry = entries.get(id);
|
|
17
|
+
if (entry === undefined) {
|
|
18
|
+
entry = { renderable: { id, position, version: 0 }, bounds: { kind: "sphere", radius, offset: [0, 0.5, 0] }, x: NaN, y: NaN, z: NaN, rotation: NaN, radius: NaN, seen: frame };
|
|
19
|
+
entries.set(id, entry);
|
|
20
|
+
}
|
|
21
|
+
const previous = entry.renderable;
|
|
22
|
+
const changed = entry.x !== position[0] || entry.y !== position[1] || entry.z !== position[2] || entry.rotation !== rotation || entry.radius !== radius || previous.overrides !== overrides || previous.layer !== layer;
|
|
23
|
+
if (changed) {
|
|
24
|
+
// A retained renderable must belong to one row: the core may keep it for always-visible processing.
|
|
25
|
+
entry.bounds.radius = radius;
|
|
26
|
+
previous.position = position;
|
|
27
|
+
previous.version += 1;
|
|
28
|
+
previous.bounds = layer === "entity" ? ENTITY_BOUNDS : entry.bounds;
|
|
29
|
+
previous.overrides = overrides;
|
|
30
|
+
previous.layer = layer;
|
|
31
|
+
entry.x = position[0];
|
|
32
|
+
entry.y = position[1];
|
|
33
|
+
entry.z = position[2];
|
|
34
|
+
entry.rotation = rotation;
|
|
35
|
+
entry.radius = radius;
|
|
36
|
+
}
|
|
37
|
+
entry.seen = frame;
|
|
38
|
+
return entry.renderable;
|
|
39
|
+
}
|
|
40
|
+
function* renderables() {
|
|
41
|
+
frame += 1;
|
|
42
|
+
for (const entity of ctx.scene.entity.list()) {
|
|
43
|
+
yield update(entity.id, entity.position, entity.rotationY, 2, config?.entities?.[entity.name], "entity");
|
|
44
|
+
}
|
|
45
|
+
for (const object of ctx.scene.object.list()) {
|
|
46
|
+
yield update(object.instanceId, object.position, object.rotationY, objectRadius(object.visual?.scale), config?.objects?.[object.catalogId], "object");
|
|
47
|
+
}
|
|
48
|
+
for (const [id, entry] of entries) {
|
|
49
|
+
if (entry.seen !== frame)
|
|
50
|
+
entries.delete(id);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const system = createVisibilitySystem({
|
|
54
|
+
renderables,
|
|
55
|
+
cameras: () => currentView === null ? EMPTY_CAMERAS : [{ id: "main", view: currentView }],
|
|
56
|
+
...(config?.culling !== undefined ? { settings: config.culling } : {}),
|
|
57
|
+
...(config?.scene !== undefined ? { sceneOverrides: config.scene } : {}),
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
system,
|
|
61
|
+
setView(view) {
|
|
62
|
+
currentView = view;
|
|
63
|
+
},
|
|
64
|
+
trackedCount() {
|
|
65
|
+
return entries.size;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EditorDocument } from "@jgengine/core/editor/types";
|
|
2
|
+
import { type WeatherLayerProps } from "./WeatherLayer.js";
|
|
3
|
+
/** Authored weather document and optional precipitation appearance and ground sampling. */
|
|
4
|
+
export interface AuthoredWeatherLayerProps {
|
|
5
|
+
document: EditorDocument;
|
|
6
|
+
rain?: WeatherLayerProps["rain"];
|
|
7
|
+
snow?: WeatherLayerProps["snow"];
|
|
8
|
+
dust?: WeatherLayerProps["dust"];
|
|
9
|
+
heightAt?: (x: number, z: number) => number;
|
|
10
|
+
/** Register actual procedural weather pool and impact query counters. */
|
|
11
|
+
diagnostics?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Render pooled precipitation from the same authored document and clock as gameplay.
|
|
14
|
+
* @capability authored-weather-rendering Render authored weather from shared gameplay samples, simulation time and shelter volumes.
|
|
15
|
+
*/
|
|
16
|
+
export declare function AuthoredWeatherLayer({ document, rain, snow, dust, heightAt, diagnostics }: AuthoredWeatherLayerProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
3
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
4
|
+
import { devtools } from "@jgengine/core/devtools/devtools";
|
|
5
|
+
import { warnOnce } from "@jgengine/core/devtools/warnOnce";
|
|
6
|
+
import { WEATHER_SHELTER_BUDGET } from "./weatherShelter.js";
|
|
7
|
+
import { WeatherLayer } from "./WeatherLayer.js";
|
|
8
|
+
/** Render pooled precipitation from the same authored document and clock as gameplay.
|
|
9
|
+
* @capability authored-weather-rendering Render authored weather from shared gameplay samples, simulation time and shelter volumes.
|
|
10
|
+
*/
|
|
11
|
+
export function AuthoredWeatherLayer({ document, rain, snow, dust, heightAt, diagnostics = false }) {
|
|
12
|
+
const ctx = useGameContext();
|
|
13
|
+
const config = document.simulation?.weather;
|
|
14
|
+
const metrics = useRef({ rain: { count: 0, capacity: 0 }, snow: { count: 0, capacity: 0 }, dust: { count: 0, capacity: 0 }, impacts: { count: 0, capacity: 0, heightQueries: 0, exposureQueries: 0, queryBudget: 0 } });
|
|
15
|
+
const mounted = config !== undefined;
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!diagnostics || !mounted)
|
|
18
|
+
return;
|
|
19
|
+
return devtools.probes.register("weatherParticles", () => ({ scope: "procedural-weather", ...structuredClone(metrics.current) }));
|
|
20
|
+
}, [diagnostics, mounted]);
|
|
21
|
+
const shelters = useMemo(() => document.volumes.filter((volume) => volume.kind === "shelter"), [document.volumes]);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (shelters.length > WEATHER_SHELTER_BUDGET)
|
|
24
|
+
warnOnce("weather-shelter-budget", `[jgengine] Weather renders the closest ${WEATHER_SHELTER_BUDGET} of ${shelters.length} shelters; gameplay uses all authored shelters.`);
|
|
25
|
+
}, [shelters.length]);
|
|
26
|
+
const sample = useCallback((x, z) => ctx.environment.sample(x, z), [ctx.environment]);
|
|
27
|
+
const exposureAt = useCallback((x, y, z) => ctx.environment.exposureAt(x, y, z), [ctx.environment]);
|
|
28
|
+
if (config === undefined)
|
|
29
|
+
return null;
|
|
30
|
+
return _jsx(WeatherLayer, { sample: sample, metrics: diagnostics ? metrics.current : undefined, shelters: shelters, rain: rain, snow: snow, dust: dust, impacts: heightAt === undefined ? false : { heightAt, exposureAt } });
|
|
31
|
+
}
|
|
@@ -26,6 +26,7 @@ export interface DustFieldProps {
|
|
|
26
26
|
*/
|
|
27
27
|
groundBias?: number;
|
|
28
28
|
timeScale?: number;
|
|
29
|
+
timeSeconds?: number | (() => number);
|
|
29
30
|
seed?: number;
|
|
30
31
|
renderOrder?: number;
|
|
31
32
|
frustumCulled?: boolean;
|
|
@@ -38,4 +39,4 @@ export interface DustFieldProps {
|
|
|
38
39
|
*
|
|
39
40
|
* @capability weather-dust airborne particulate that turns still air into visible wind
|
|
40
41
|
*/
|
|
41
|
-
export declare function DustField({ count, density, budget, volume, wind, origin, followCamera, speed, size, sway, opacity, color, groundBias, timeScale, seed, renderOrder, frustumCulled, }: DustFieldProps): import("react").JSX.Element;
|
|
42
|
+
export declare function DustField({ count, density, budget, volume, wind, origin, followCamera, speed, size, sway, opacity, color, groundBias, timeScale, timeSeconds, seed, renderOrder, frustumCulled, }: DustFieldProps): import("react").JSX.Element;
|
|
@@ -4,6 +4,7 @@ import * as THREE from "three";
|
|
|
4
4
|
import { useDisposable } from "../render/useDisposable.js";
|
|
5
5
|
import { createWeatherQuadGeometry } from "./weatherGeometry.js";
|
|
6
6
|
import { DEFAULT_DUST_COUNT, DEFAULT_DUST_DENSITY, resolveWeatherInstanceCount } from "./weatherMath.js";
|
|
7
|
+
import { WEATHER_SHELTER_SHADER } from "./weatherShelter.js";
|
|
7
8
|
import { useWeatherUniformSet } from "./weatherUniforms.js";
|
|
8
9
|
const DEFAULT_VOLUME = [70, 22, 70];
|
|
9
10
|
const DEFAULT_ORIGIN = [0, 0, 0];
|
|
@@ -16,13 +17,16 @@ const DEFAULT_DUST_COLOR = "#c8a878";
|
|
|
16
17
|
*
|
|
17
18
|
* @capability weather-dust airborne particulate that turns still air into visible wind
|
|
18
19
|
*/
|
|
19
|
-
export function DustField({ count = DEFAULT_DUST_COUNT, density = DEFAULT_DUST_DENSITY, budget, volume = DEFAULT_VOLUME, wind, origin = DEFAULT_ORIGIN, followCamera = true, speed = 2.4, size = 0.16, sway = 0.5, opacity = 0.3, color = DEFAULT_DUST_COLOR, groundBias = 0.65, timeScale, seed = 51407, renderOrder = 10, frustumCulled, }) {
|
|
20
|
+
export function DustField({ count = DEFAULT_DUST_COUNT, density = DEFAULT_DUST_DENSITY, budget, volume = DEFAULT_VOLUME, wind, origin = DEFAULT_ORIGIN, followCamera = true, speed = 2.4, size = 0.16, sway = 0.5, opacity = 0.3, color = DEFAULT_DUST_COLOR, groundBias = 0.65, timeScale, timeSeconds, seed = 51407, renderOrder = 10, frustumCulled, }) {
|
|
20
21
|
const { camera } = useThree();
|
|
21
|
-
const shared = useWeatherUniformSet({ wind, timeScale });
|
|
22
|
+
const shared = useWeatherUniformSet({ wind, timeScale, timeSeconds });
|
|
22
23
|
const geometry = useDisposable(() => createWeatherQuadGeometry(count, seed), [count, seed]);
|
|
23
24
|
const material = useDisposable(() => {
|
|
24
25
|
const uniforms = {
|
|
25
26
|
uTime: shared.time,
|
|
27
|
+
uShelterCenters: shared.shelterCenters,
|
|
28
|
+
uShelterShapes: shared.shelterShapes,
|
|
29
|
+
uShelterCount: shared.shelterCount,
|
|
26
30
|
uWind: shared.wind,
|
|
27
31
|
uAnchor: { value: new THREE.Vector3() },
|
|
28
32
|
uVolume: { value: new THREE.Vector3() },
|
|
@@ -39,6 +43,7 @@ export function DustField({ count = DEFAULT_DUST_COUNT, density = DEFAULT_DUST_D
|
|
|
39
43
|
depthWrite: false,
|
|
40
44
|
blending: THREE.NormalBlending,
|
|
41
45
|
vertexShader: `
|
|
46
|
+
${WEATHER_SHELTER_SHADER}
|
|
42
47
|
uniform float uTime;
|
|
43
48
|
uniform vec3 uWind;
|
|
44
49
|
uniform vec3 uAnchor;
|
|
@@ -51,6 +56,7 @@ export function DustField({ count = DEFAULT_DUST_COUNT, density = DEFAULT_DUST_D
|
|
|
51
56
|
attribute vec3 aSpawn;
|
|
52
57
|
attribute float aDrift;
|
|
53
58
|
|
|
59
|
+
varying float vExposure;
|
|
54
60
|
varying vec2 vUv;
|
|
55
61
|
varying float vDrift;
|
|
56
62
|
varying float vFade;
|
|
@@ -76,6 +82,7 @@ export function DustField({ count = DEFAULT_DUST_COUNT, density = DEFAULT_DUST_D
|
|
|
76
82
|
vec3 travel = normalize(vec3(uWind.x, 0.0, uWind.z) + vec3(0.0001, 0.0, 0.0)) * carry;
|
|
77
83
|
vec3 local = spawn * uVolume + (uWind + travel) * uTime + bob;
|
|
78
84
|
vec3 worldCenter = mod(local - origin, uVolume) + origin;
|
|
85
|
+
vExposure = weatherExposure(worldCenter);
|
|
79
86
|
|
|
80
87
|
// Fade at the top of the volume so recycled motes do not pop into view against the sky.
|
|
81
88
|
float height = (worldCenter.y - origin.y) / max(uVolume.y, 0.001);
|
|
@@ -93,6 +100,7 @@ export function DustField({ count = DEFAULT_DUST_COUNT, density = DEFAULT_DUST_D
|
|
|
93
100
|
uniform float uOpacity;
|
|
94
101
|
uniform vec3 uColor;
|
|
95
102
|
|
|
103
|
+
varying float vExposure;
|
|
96
104
|
varying vec2 vUv;
|
|
97
105
|
varying float vDrift;
|
|
98
106
|
varying float vFade;
|
|
@@ -102,6 +110,7 @@ export function DustField({ count = DEFAULT_DUST_COUNT, density = DEFAULT_DUST_D
|
|
|
102
110
|
// No hard core: dust is diffuse, and a bright centre reads as snow.
|
|
103
111
|
float puff = smoothstep(1.0, 0.05, distanceFromCenter);
|
|
104
112
|
float alpha = puff * puff * uOpacity * (0.35 + 0.65 * vDrift) * vFade;
|
|
113
|
+
alpha *= vExposure;
|
|
105
114
|
if (alpha < 0.001) discard;
|
|
106
115
|
gl_FragColor = vec4(uColor, alpha);
|
|
107
116
|
}
|
|
@@ -123,7 +132,12 @@ export function DustField({ count = DEFAULT_DUST_COUNT, density = DEFAULT_DUST_D
|
|
|
123
132
|
uniforms.uOpacity.value = opacity;
|
|
124
133
|
uniforms.uGroundBias.value = groundBias;
|
|
125
134
|
uniforms.uColor.value.set(color);
|
|
126
|
-
geometry.instanceCount = resolveWeatherInstanceCount(count, density, budget);
|
|
135
|
+
geometry.instanceCount = resolveWeatherInstanceCount(count, density * shared.dust.value, budget);
|
|
136
|
+
const metrics = shared.metrics?.dust;
|
|
137
|
+
if (metrics !== undefined) {
|
|
138
|
+
metrics.count = geometry.instanceCount;
|
|
139
|
+
metrics.capacity = geometry.getAttribute("aSpawn").count;
|
|
140
|
+
}
|
|
127
141
|
});
|
|
128
142
|
return (_jsx("mesh", { geometry: geometry, material: material, frustumCulled: frustumCulled ?? !followCamera, renderOrder: renderOrder }));
|
|
129
143
|
}
|
|
@@ -2,7 +2,7 @@ import * as THREE from "three";
|
|
|
2
2
|
import type { FireGrid } from "@jgengine/core/world/weather";
|
|
3
3
|
export interface FireSpreadLayerProps {
|
|
4
4
|
grid: FireGrid;
|
|
5
|
-
cellSize
|
|
5
|
+
cellSize?: number;
|
|
6
6
|
origin?: readonly [number, number];
|
|
7
7
|
/** Sample ground height so flames sit on terrain; defaults to y=0. */
|
|
8
8
|
heightAt?: (x: number, z: number) => number;
|
|
@@ -10,5 +10,7 @@ export interface FireSpreadLayerProps {
|
|
|
10
10
|
flameHeight?: number;
|
|
11
11
|
burningColor?: THREE.ColorRepresentation;
|
|
12
12
|
emberColor?: THREE.ColorRepresentation;
|
|
13
|
+
/** Absolute authoritative simulation clock; use for pause and snapshot restore. */
|
|
14
|
+
timeSeconds?: () => number;
|
|
13
15
|
}
|
|
14
|
-
export declare function FireSpreadLayer({ grid, cellSize, origin, heightAt, flameHeight, burningColor, emberColor, }: FireSpreadLayerProps): import("react").JSX.Element;
|
|
16
|
+
export declare function FireSpreadLayer({ grid, cellSize, origin, heightAt, flameHeight, burningColor, emberColor, timeSeconds, }: FireSpreadLayerProps): import("react").JSX.Element;
|
|
@@ -3,8 +3,7 @@ import { useEffect, useMemo, useRef } from "react";
|
|
|
3
3
|
import { useFrame } from "@react-three/fiber";
|
|
4
4
|
import * as THREE from "three";
|
|
5
5
|
import { setBillboardQuaternion } from "./fireSpreadPose.js";
|
|
6
|
-
|
|
7
|
-
export function FireSpreadLayer({ grid, cellSize, origin = [0, 0], heightAt, flameHeight = 1.6, burningColor = "#ff6a1a", emberColor = "#4a1206", }) {
|
|
6
|
+
export function FireSpreadLayer({ grid, cellSize = grid.cellSize, origin = grid.origin, heightAt, flameHeight = 1.6, burningColor = "#ff6a1a", emberColor = "#4a1206", timeSeconds, }) {
|
|
8
7
|
const flames = useRef(null);
|
|
9
8
|
const scorch = useRef(null);
|
|
10
9
|
const capacity = grid.cols * grid.rows;
|
|
@@ -33,13 +32,12 @@ export function FireSpreadLayer({ grid, cellSize, origin = [0, 0], heightAt, fla
|
|
|
33
32
|
const scorchMesh = scorch.current;
|
|
34
33
|
if (flameMesh === null || scorchMesh === null)
|
|
35
34
|
return;
|
|
36
|
-
const
|
|
37
|
-
const flicker = 0.85 + 0.15 * Math.sin(state.clock.elapsedTime * 12);
|
|
35
|
+
const flicker = 0.85 + 0.15 * Math.sin((timeSeconds?.() ?? state.clock.elapsedTime) * 12);
|
|
38
36
|
let flameIndex = 0;
|
|
39
37
|
let scorchIndex = 0;
|
|
40
38
|
for (let row = 0; row < grid.rows; row += 1) {
|
|
41
39
|
for (let col = 0; col < grid.cols; col += 1) {
|
|
42
|
-
const cell =
|
|
40
|
+
const cell = grid.cell(col, row);
|
|
43
41
|
const x = origin[0] + col * cellSize;
|
|
44
42
|
const z = origin[1] + row * cellSize;
|
|
45
43
|
const groundY = heightAt?.(x, z) ?? 0;
|
|
@@ -60,12 +58,8 @@ export function FireSpreadLayer({ grid, cellSize, origin = [0, 0], heightAt, fla
|
|
|
60
58
|
}
|
|
61
59
|
}
|
|
62
60
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
for (let i = scorchIndex; i < capacity; i += 1)
|
|
66
|
-
scorchMesh.setMatrixAt(i, HIDDEN);
|
|
67
|
-
flameMesh.count = capacity;
|
|
68
|
-
scorchMesh.count = capacity;
|
|
61
|
+
flameMesh.count = flameIndex;
|
|
62
|
+
scorchMesh.count = scorchIndex;
|
|
69
63
|
flameMesh.instanceMatrix.needsUpdate = true;
|
|
70
64
|
scorchMesh.instanceMatrix.needsUpdate = true;
|
|
71
65
|
});
|
|
@@ -13,5 +13,8 @@ export interface LightningStrikeProps {
|
|
|
13
13
|
jaggedness?: number;
|
|
14
14
|
impactLight?: number;
|
|
15
15
|
renderOrder?: number;
|
|
16
|
+
/** Supply both fields to replay the strike from authoritative simulation state. */
|
|
17
|
+
timeSeconds?: () => number;
|
|
18
|
+
startedAt?: number;
|
|
16
19
|
}
|
|
17
|
-
export declare function LightningStrike({ origin, target, strikeKey, seed, visible, duration, color, glow, branches, jaggedness, impactLight, renderOrder, }: LightningStrikeProps): import("react").JSX.Element;
|
|
20
|
+
export declare function LightningStrike({ origin, target, strikeKey, seed, visible, duration, color, glow, branches, jaggedness, impactLight, renderOrder, timeSeconds, startedAt, }: LightningStrikeProps): import("react").JSX.Element;
|
|
@@ -3,6 +3,7 @@ import { useEffect, useMemo, useRef } from "react";
|
|
|
3
3
|
import { useFrame } from "@react-three/fiber";
|
|
4
4
|
import * as THREE from "three";
|
|
5
5
|
import { useDisposable } from "../render/useDisposable.js";
|
|
6
|
+
import { lightningStrikeEnvelope } from "./weatherMath.js";
|
|
6
7
|
const DEFAULT_COLOR = "#bdd4ff";
|
|
7
8
|
function createRandom(seed) {
|
|
8
9
|
let state = seed >>> 0;
|
|
@@ -47,7 +48,7 @@ function createLightningSegments(origin, target, seed, branches, jaggedness) {
|
|
|
47
48
|
for (let index = 0; index < points.length - 1; index += 1) {
|
|
48
49
|
segments.push({ from: points[index], to: points[index + 1] });
|
|
49
50
|
}
|
|
50
|
-
const branchCount = Math.max(0, Math.floor(branches));
|
|
51
|
+
const branchCount = Math.max(0, Math.min(64, Math.floor(branches)));
|
|
51
52
|
for (let index = 0; index < branchCount; index += 1) {
|
|
52
53
|
const sourceIndex = 2 + Math.floor(random() * Math.max(1, points.length - 5));
|
|
53
54
|
const source = points[sourceIndex];
|
|
@@ -78,8 +79,9 @@ function writeLightningGeometry(geometry, segments) {
|
|
|
78
79
|
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
|
79
80
|
geometry.computeBoundingSphere();
|
|
80
81
|
}
|
|
81
|
-
export function LightningStrike({ origin, target, strikeKey = 0, seed = 451, visible = true, duration = 0.18, color = DEFAULT_COLOR, glow = 2.4, branches = 5, jaggedness = 0.08, impactLight = 26, renderOrder = 20, }) {
|
|
82
|
-
const
|
|
82
|
+
export function LightningStrike({ origin, target, strikeKey = 0, seed = 451, visible = true, duration = 0.18, color = DEFAULT_COLOR, glow = 2.4, branches = 5, jaggedness = 0.08, impactLight = 26, renderOrder = 20, timeSeconds, startedAt, }) {
|
|
83
|
+
const localTime = useRef(0);
|
|
84
|
+
const startRef = useRef(null);
|
|
83
85
|
const lightRef = useRef(null);
|
|
84
86
|
const geometry = useDisposable(() => new THREE.BufferGeometry(), []);
|
|
85
87
|
const material = useDisposable(() => new THREE.LineBasicMaterial({
|
|
@@ -97,28 +99,22 @@ export function LightningStrike({ origin, target, strikeKey = 0, seed = 451, vis
|
|
|
97
99
|
}, [geometry, material, renderOrder]);
|
|
98
100
|
useEffect(() => {
|
|
99
101
|
if (!visible) {
|
|
100
|
-
|
|
102
|
+
startRef.current = null;
|
|
101
103
|
material.opacity = 0;
|
|
102
104
|
if (lightRef.current !== null)
|
|
103
105
|
lightRef.current.intensity = 0;
|
|
104
106
|
return;
|
|
105
107
|
}
|
|
106
108
|
writeLightningGeometry(geometry, createLightningSegments(origin, target, seed ^ hashStrikeKey(strikeKey), branches, jaggedness));
|
|
107
|
-
|
|
108
|
-
}, [branches, duration, geometry, jaggedness, material, origin, seed, strikeKey, target, visible]);
|
|
109
|
+
startRef.current = startedAt ?? timeSeconds?.() ?? localTime.current;
|
|
110
|
+
}, [branches, duration, geometry, jaggedness, material, origin, seed, strikeKey, target, visible, startedAt, timeSeconds]);
|
|
109
111
|
useFrame((_state, delta) => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
lifeRef.current = Math.max(0, lifeRef.current - delta);
|
|
117
|
-
const amount = duration <= 0 ? 0 : lifeRef.current / duration;
|
|
118
|
-
const flicker = 0.62 + Math.random() * 0.38;
|
|
119
|
-
material.opacity = amount * glow * flicker;
|
|
112
|
+
localTime.current += delta;
|
|
113
|
+
const now = timeSeconds?.() ?? localTime.current;
|
|
114
|
+
const amount = visible && startRef.current !== null ? lightningStrikeEnvelope(now, startRef.current, duration, seed ^ hashStrikeKey(strikeKey)) : 0;
|
|
115
|
+
material.opacity = amount * glow;
|
|
120
116
|
if (lightRef.current !== null)
|
|
121
|
-
lightRef.current.intensity = amount * impactLight
|
|
117
|
+
lightRef.current.intensity = amount * impactLight;
|
|
122
118
|
});
|
|
123
119
|
return (_jsxs(_Fragment, { children: [_jsx("primitive", { object: line }), _jsx("pointLight", { ref: lightRef, position: [target[0], target[1], target[2]], color: color, intensity: 0, distance: 56, decay: 2 })] }));
|
|
124
120
|
}
|
|
@@ -15,8 +15,9 @@ export interface RainFieldProps {
|
|
|
15
15
|
color?: THREE.ColorRepresentation;
|
|
16
16
|
lightning?: number;
|
|
17
17
|
timeScale?: number;
|
|
18
|
+
timeSeconds?: number | (() => number);
|
|
18
19
|
seed?: number;
|
|
19
20
|
renderOrder?: number;
|
|
20
21
|
frustumCulled?: boolean;
|
|
21
22
|
}
|
|
22
|
-
export declare function RainField({ count, density, budget, volume, wind, origin, followCamera, speed, length, width, opacity, color, lightning, timeScale, seed, renderOrder, frustumCulled, }: RainFieldProps): import("react").JSX.Element;
|
|
23
|
+
export declare function RainField({ count, density, budget, volume, wind, origin, followCamera, speed, length, width, opacity, color, lightning, timeScale, timeSeconds, seed, renderOrder, frustumCulled, }: RainFieldProps): import("react").JSX.Element;
|
|
@@ -4,17 +4,21 @@ import * as THREE from "three";
|
|
|
4
4
|
import { useDisposable } from "../render/useDisposable.js";
|
|
5
5
|
import { createWeatherQuadGeometry } from "./weatherGeometry.js";
|
|
6
6
|
import { DEFAULT_RAIN_COUNT, DEFAULT_RAIN_DENSITY, resolveWeatherInstanceCount } from "./weatherMath.js";
|
|
7
|
+
import { WEATHER_SHELTER_SHADER } from "./weatherShelter.js";
|
|
7
8
|
import { useWeatherUniformSet } from "./weatherUniforms.js";
|
|
8
9
|
const DEFAULT_VOLUME = [56, 42, 56];
|
|
9
10
|
const DEFAULT_ORIGIN = [0, 0, 0];
|
|
10
11
|
const DEFAULT_RAIN_COLOR = "#b8c4d8";
|
|
11
|
-
export function RainField({ count = DEFAULT_RAIN_COUNT, density = DEFAULT_RAIN_DENSITY, budget, volume = DEFAULT_VOLUME, wind, origin = DEFAULT_ORIGIN, followCamera = true, speed = 22, length = 1.35, width = 0.018, opacity = 0.48, color = DEFAULT_RAIN_COLOR, lightning, timeScale, seed = 11939, renderOrder = 10, frustumCulled, }) {
|
|
12
|
+
export function RainField({ count = DEFAULT_RAIN_COUNT, density = DEFAULT_RAIN_DENSITY, budget, volume = DEFAULT_VOLUME, wind, origin = DEFAULT_ORIGIN, followCamera = true, speed = 22, length = 1.35, width = 0.018, opacity = 0.48, color = DEFAULT_RAIN_COLOR, lightning, timeScale, timeSeconds, seed = 11939, renderOrder = 10, frustumCulled, }) {
|
|
12
13
|
const { camera } = useThree();
|
|
13
|
-
const shared = useWeatherUniformSet({ wind, lightning, timeScale });
|
|
14
|
+
const shared = useWeatherUniformSet({ wind, lightning, timeScale, timeSeconds });
|
|
14
15
|
const geometry = useDisposable(() => createWeatherQuadGeometry(count, seed), [count, seed]);
|
|
15
16
|
const material = useDisposable(() => {
|
|
16
17
|
const uniforms = {
|
|
17
18
|
uTime: shared.time,
|
|
19
|
+
uShelterCenters: shared.shelterCenters,
|
|
20
|
+
uShelterShapes: shared.shelterShapes,
|
|
21
|
+
uShelterCount: shared.shelterCount,
|
|
18
22
|
uWind: shared.wind,
|
|
19
23
|
uLightning: shared.lightning,
|
|
20
24
|
uAnchor: { value: new THREE.Vector3() },
|
|
@@ -31,6 +35,7 @@ export function RainField({ count = DEFAULT_RAIN_COUNT, density = DEFAULT_RAIN_D
|
|
|
31
35
|
depthWrite: false,
|
|
32
36
|
blending: THREE.NormalBlending,
|
|
33
37
|
vertexShader: `
|
|
38
|
+
${WEATHER_SHELTER_SHADER}
|
|
34
39
|
uniform float uTime;
|
|
35
40
|
uniform vec3 uWind;
|
|
36
41
|
uniform vec3 uAnchor;
|
|
@@ -42,6 +47,7 @@ export function RainField({ count = DEFAULT_RAIN_COUNT, density = DEFAULT_RAIN_D
|
|
|
42
47
|
attribute vec3 aSpawn;
|
|
43
48
|
attribute float aDrift;
|
|
44
49
|
|
|
50
|
+
varying float vExposure;
|
|
45
51
|
varying vec2 vUv;
|
|
46
52
|
varying float vDrift;
|
|
47
53
|
|
|
@@ -54,6 +60,7 @@ export function RainField({ count = DEFAULT_RAIN_COUNT, density = DEFAULT_RAIN_D
|
|
|
54
60
|
vec3 velocity = vec3(uWind.x, -speed, uWind.z);
|
|
55
61
|
vec3 local = aSpawn * uVolume + velocity * uTime;
|
|
56
62
|
vec3 worldCenter = mod(local - origin, uVolume) + origin;
|
|
63
|
+
vExposure = weatherExposure(worldCenter);
|
|
57
64
|
vec3 direction = normalize(velocity);
|
|
58
65
|
vec3 cameraRay = normalize(cameraPosition - worldCenter);
|
|
59
66
|
vec3 sideRaw = cross(direction, cameraRay);
|
|
@@ -69,6 +76,7 @@ export function RainField({ count = DEFAULT_RAIN_COUNT, density = DEFAULT_RAIN_D
|
|
|
69
76
|
uniform vec3 uColor;
|
|
70
77
|
uniform float uLightning;
|
|
71
78
|
|
|
79
|
+
varying float vExposure;
|
|
72
80
|
varying vec2 vUv;
|
|
73
81
|
varying float vDrift;
|
|
74
82
|
|
|
@@ -76,6 +84,7 @@ export function RainField({ count = DEFAULT_RAIN_COUNT, density = DEFAULT_RAIN_D
|
|
|
76
84
|
float across = smoothstep(0.0, 0.45, vUv.x) * smoothstep(1.0, 0.55, vUv.x);
|
|
77
85
|
float along = smoothstep(0.0, 0.28, vUv.y) * smoothstep(1.0, 0.58, vUv.y);
|
|
78
86
|
float alpha = across * along * uOpacity * (0.55 + 0.45 * vDrift);
|
|
87
|
+
alpha *= vExposure;
|
|
79
88
|
if (alpha < 0.001) discard;
|
|
80
89
|
vec3 litColor = uColor * (1.0 + uLightning * 2.25);
|
|
81
90
|
gl_FragColor = vec4(litColor, alpha);
|
|
@@ -97,7 +106,12 @@ export function RainField({ count = DEFAULT_RAIN_COUNT, density = DEFAULT_RAIN_D
|
|
|
97
106
|
uniforms.uWidth.value = width;
|
|
98
107
|
uniforms.uOpacity.value = opacity;
|
|
99
108
|
uniforms.uColor.value.set(color);
|
|
100
|
-
geometry.instanceCount = resolveWeatherInstanceCount(count, density, budget);
|
|
109
|
+
geometry.instanceCount = resolveWeatherInstanceCount(count, density * shared.rain.value, budget);
|
|
110
|
+
const metrics = shared.metrics?.rain;
|
|
111
|
+
if (metrics !== undefined) {
|
|
112
|
+
metrics.count = geometry.instanceCount;
|
|
113
|
+
metrics.capacity = geometry.getAttribute("aSpawn").count;
|
|
114
|
+
}
|
|
101
115
|
});
|
|
102
116
|
return (_jsx("mesh", { geometry: geometry, material: material, frustumCulled: frustumCulled ?? !followCamera, renderOrder: renderOrder }));
|
|
103
117
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
/** Bounded ripple pool with terrain height and sky exposure query adapters. */
|
|
3
|
+
export interface RainImpactFieldProps {
|
|
4
|
+
/** Fixed splash pool, clamped to 256. Default 64. */
|
|
5
|
+
count?: number;
|
|
6
|
+
extent?: number;
|
|
7
|
+
heightAt: (x: number, z: number) => number;
|
|
8
|
+
/** Sky exposure 0..1; sheltered surfaces produce no splashes. */
|
|
9
|
+
exposureAt?: (x: number, y: number, z: number) => number;
|
|
10
|
+
seed?: number;
|
|
11
|
+
color?: THREE.ColorRepresentation;
|
|
12
|
+
}
|
|
13
|
+
/** Bounded ground ripples sharing precipitation intensity and authoritative animation time.
|
|
14
|
+
* @capability rain-ground-ripples Render bounded rain impacts on exposed terrain using the shared precipitation clock.
|
|
15
|
+
*/
|
|
16
|
+
export declare function RainImpactField({ count, extent, heightAt, exposureAt, seed, color }: RainImpactFieldProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame } from "@react-three/fiber";
|
|
3
|
+
import * as THREE from "three";
|
|
4
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
5
|
+
import { createWeatherQuadGeometry } from "./weatherGeometry.js";
|
|
6
|
+
import { useWeatherUniformSet } from "./weatherUniforms.js";
|
|
7
|
+
/** Bounded ground ripples sharing precipitation intensity and authoritative animation time.
|
|
8
|
+
* @capability rain-ground-ripples Render bounded rain impacts on exposed terrain using the shared precipitation clock.
|
|
9
|
+
*/
|
|
10
|
+
export function RainImpactField({ count = 64, extent = 48, heightAt, exposureAt, seed = 11939, color = "#b8c4d8" }) {
|
|
11
|
+
const capacity = Math.max(0, Math.min(256, Math.floor(Number.isFinite(count) ? count : 64)));
|
|
12
|
+
const size = Math.max(1, Number.isFinite(extent) ? extent : 48);
|
|
13
|
+
const shared = useWeatherUniformSet();
|
|
14
|
+
const geometry = useDisposable(() => {
|
|
15
|
+
const value = createWeatherQuadGeometry(capacity, seed);
|
|
16
|
+
value.setAttribute("aGround", new THREE.InstancedBufferAttribute(new Float32Array(capacity), 1));
|
|
17
|
+
value.setAttribute("aExposure", new THREE.InstancedBufferAttribute(new Float32Array(capacity), 1));
|
|
18
|
+
value.instanceCount = capacity;
|
|
19
|
+
return value;
|
|
20
|
+
}, [capacity, seed]);
|
|
21
|
+
const material = useDisposable(() => new THREE.ShaderMaterial({
|
|
22
|
+
transparent: true,
|
|
23
|
+
side: THREE.DoubleSide,
|
|
24
|
+
depthWrite: false,
|
|
25
|
+
uniforms: { uTime: shared.time, uRain: shared.rain, uAnchor: { value: new THREE.Vector2() }, uExtent: { value: size }, uColor: { value: new THREE.Color(color) } },
|
|
26
|
+
vertexShader: `
|
|
27
|
+
uniform float uTime;
|
|
28
|
+
uniform float uExtent;
|
|
29
|
+
uniform vec2 uAnchor;
|
|
30
|
+
attribute vec3 aSpawn;
|
|
31
|
+
attribute float aDrift;
|
|
32
|
+
attribute float aGround;
|
|
33
|
+
attribute float aExposure;
|
|
34
|
+
varying vec2 vUv;
|
|
35
|
+
varying float vPhase;
|
|
36
|
+
varying float vExposure;
|
|
37
|
+
void main() {
|
|
38
|
+
vUv = uv;
|
|
39
|
+
vPhase = fract(uTime * (1.6 + aDrift * 0.8) + aDrift);
|
|
40
|
+
vExposure = aExposure;
|
|
41
|
+
float radius = 0.08 + vPhase * 0.45;
|
|
42
|
+
vec2 center = uAnchor + (aSpawn.xz - 0.5) * uExtent;
|
|
43
|
+
vec3 world = vec3(center.x + position.x * radius, aGround + 0.015, center.y + position.y * radius);
|
|
44
|
+
gl_Position = projectionMatrix * viewMatrix * vec4(world, 1.0);
|
|
45
|
+
}
|
|
46
|
+
`,
|
|
47
|
+
fragmentShader: `
|
|
48
|
+
uniform float uRain;
|
|
49
|
+
uniform vec3 uColor;
|
|
50
|
+
varying vec2 vUv;
|
|
51
|
+
varying float vPhase;
|
|
52
|
+
varying float vExposure;
|
|
53
|
+
void main() {
|
|
54
|
+
float radius = length(vUv - 0.5) * 2.0;
|
|
55
|
+
float ring = smoothstep(0.65, 0.8, radius) * (1.0 - smoothstep(0.84, 1.0, radius));
|
|
56
|
+
float alpha = ring * (1.0 - vPhase) * min(1.0, uRain) * vExposure * 0.45;
|
|
57
|
+
if (alpha < 0.001) discard;
|
|
58
|
+
gl_FragColor = vec4(uColor, alpha);
|
|
59
|
+
}
|
|
60
|
+
`,
|
|
61
|
+
}), [shared, size, color]);
|
|
62
|
+
useFrame((state) => {
|
|
63
|
+
const anchorX = Math.round(state.camera.position.x / 8) * 8;
|
|
64
|
+
const anchorZ = Math.round(state.camera.position.z / 8) * 8;
|
|
65
|
+
material.uniforms.uAnchor.value.set(anchorX, anchorZ);
|
|
66
|
+
const spawn = geometry.getAttribute("aSpawn");
|
|
67
|
+
const ground = geometry.getAttribute("aGround");
|
|
68
|
+
const exposure = geometry.getAttribute("aExposure");
|
|
69
|
+
for (let i = 0; i < capacity; i += 1) {
|
|
70
|
+
const x = anchorX + (spawn.getX(i) - 0.5) * size;
|
|
71
|
+
const z = anchorZ + (spawn.getZ(i) - 0.5) * size;
|
|
72
|
+
const y = heightAt(x, z);
|
|
73
|
+
ground.setX(i, Number.isFinite(y) ? y : 0);
|
|
74
|
+
const open = exposureAt?.(x, y, z) ?? 1;
|
|
75
|
+
exposure.setX(i, Math.max(0, Math.min(1, Number.isFinite(open) ? open : 0)));
|
|
76
|
+
}
|
|
77
|
+
ground.needsUpdate = true;
|
|
78
|
+
exposure.needsUpdate = true;
|
|
79
|
+
const metrics = shared.metrics?.impacts;
|
|
80
|
+
if (metrics !== undefined) {
|
|
81
|
+
metrics.count = geometry.instanceCount;
|
|
82
|
+
metrics.capacity = geometry.getAttribute("aSpawn").count;
|
|
83
|
+
metrics.heightQueries = capacity;
|
|
84
|
+
metrics.exposureQueries = exposureAt === undefined ? 0 : capacity;
|
|
85
|
+
metrics.queryBudget = capacity * (exposureAt === undefined ? 1 : 2);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return _jsx("mesh", { geometry: geometry, material: material, frustumCulled: false, renderOrder: 9 });
|
|
89
|
+
}
|