@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
|
@@ -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
|
+
}
|
|
@@ -14,8 +14,9 @@ export interface SnowFieldProps {
|
|
|
14
14
|
opacity?: number;
|
|
15
15
|
color?: THREE.ColorRepresentation;
|
|
16
16
|
timeScale?: number;
|
|
17
|
+
timeSeconds?: number | (() => number);
|
|
17
18
|
seed?: number;
|
|
18
19
|
renderOrder?: number;
|
|
19
20
|
frustumCulled?: boolean;
|
|
20
21
|
}
|
|
21
|
-
export declare function SnowField({ count, density, budget, volume, wind, origin, followCamera, speed, size, sway, opacity, color, timeScale, seed, renderOrder, frustumCulled, }: SnowFieldProps): import("react").JSX.Element;
|
|
22
|
+
export declare function SnowField({ count, density, budget, volume, wind, origin, followCamera, speed, size, sway, opacity, color, timeScale, timeSeconds, seed, renderOrder, frustumCulled, }: SnowFieldProps): 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_SNOW_COUNT, DEFAULT_SNOW_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 = [52, 38, 52];
|
|
9
10
|
const DEFAULT_ORIGIN = [0, 0, 0];
|
|
10
11
|
const DEFAULT_SNOW_COLOR = "#ffffff";
|
|
11
|
-
export function SnowField({ count = DEFAULT_SNOW_COUNT, density = DEFAULT_SNOW_DENSITY, budget, volume = DEFAULT_VOLUME, wind, origin = DEFAULT_ORIGIN, followCamera = true, speed = 3.2, size = 0.11, sway = 0.62, opacity = 0.86, color = DEFAULT_SNOW_COLOR, timeScale, seed = 72931, renderOrder = 11, frustumCulled, }) {
|
|
12
|
+
export function SnowField({ count = DEFAULT_SNOW_COUNT, density = DEFAULT_SNOW_DENSITY, budget, volume = DEFAULT_VOLUME, wind, origin = DEFAULT_ORIGIN, followCamera = true, speed = 3.2, size = 0.11, sway = 0.62, opacity = 0.86, color = DEFAULT_SNOW_COLOR, timeScale, timeSeconds, seed = 72931, renderOrder = 11, frustumCulled, }) {
|
|
12
13
|
const { camera } = useThree();
|
|
13
|
-
const shared = useWeatherUniformSet({ wind, timeScale });
|
|
14
|
+
const shared = useWeatherUniformSet({ wind, 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
|
uAnchor: { value: new THREE.Vector3() },
|
|
20
24
|
uVolume: { value: new THREE.Vector3() },
|
|
@@ -30,6 +34,7 @@ export function SnowField({ count = DEFAULT_SNOW_COUNT, density = DEFAULT_SNOW_D
|
|
|
30
34
|
depthWrite: false,
|
|
31
35
|
blending: THREE.NormalBlending,
|
|
32
36
|
vertexShader: `
|
|
37
|
+
${WEATHER_SHELTER_SHADER}
|
|
33
38
|
uniform float uTime;
|
|
34
39
|
uniform vec3 uWind;
|
|
35
40
|
uniform vec3 uAnchor;
|
|
@@ -41,6 +46,7 @@ export function SnowField({ count = DEFAULT_SNOW_COUNT, density = DEFAULT_SNOW_D
|
|
|
41
46
|
attribute vec3 aSpawn;
|
|
42
47
|
attribute float aDrift;
|
|
43
48
|
|
|
49
|
+
varying float vExposure;
|
|
44
50
|
varying vec2 vUv;
|
|
45
51
|
varying float vDrift;
|
|
46
52
|
|
|
@@ -58,6 +64,7 @@ export function SnowField({ count = DEFAULT_SNOW_COUNT, density = DEFAULT_SNOW_D
|
|
|
58
64
|
) * uSway * (0.45 + 0.55 * aDrift);
|
|
59
65
|
vec3 local = aSpawn * uVolume + vec3(uWind.x, -fallSpeed, uWind.z) * uTime + wander;
|
|
60
66
|
vec3 worldCenter = mod(local - origin, uVolume) + origin;
|
|
67
|
+
vExposure = weatherExposure(worldCenter);
|
|
61
68
|
float flakeSize = uSize * (0.48 + 1.08 * aDrift);
|
|
62
69
|
vec3 right = vec3(viewMatrix[0][0], viewMatrix[1][0], viewMatrix[2][0]);
|
|
63
70
|
vec3 up = vec3(viewMatrix[0][1], viewMatrix[1][1], viewMatrix[2][1]);
|
|
@@ -70,6 +77,7 @@ export function SnowField({ count = DEFAULT_SNOW_COUNT, density = DEFAULT_SNOW_D
|
|
|
70
77
|
uniform float uOpacity;
|
|
71
78
|
uniform vec3 uColor;
|
|
72
79
|
|
|
80
|
+
varying float vExposure;
|
|
73
81
|
varying vec2 vUv;
|
|
74
82
|
varying float vDrift;
|
|
75
83
|
|
|
@@ -78,6 +86,7 @@ export function SnowField({ count = DEFAULT_SNOW_COUNT, density = DEFAULT_SNOW_D
|
|
|
78
86
|
float disc = smoothstep(1.0, 0.12, distanceFromCenter);
|
|
79
87
|
float core = smoothstep(0.54, 0.0, distanceFromCenter) * 0.42;
|
|
80
88
|
float alpha = (disc + core) * uOpacity * (0.56 + 0.44 * vDrift);
|
|
89
|
+
alpha *= vExposure;
|
|
81
90
|
if (alpha < 0.001) discard;
|
|
82
91
|
gl_FragColor = vec4(uColor, alpha);
|
|
83
92
|
}
|
|
@@ -98,7 +107,12 @@ export function SnowField({ count = DEFAULT_SNOW_COUNT, density = DEFAULT_SNOW_D
|
|
|
98
107
|
uniforms.uSway.value = sway;
|
|
99
108
|
uniforms.uOpacity.value = opacity;
|
|
100
109
|
uniforms.uColor.value.set(color);
|
|
101
|
-
geometry.instanceCount = resolveWeatherInstanceCount(count, density, budget);
|
|
110
|
+
geometry.instanceCount = resolveWeatherInstanceCount(count, density * shared.snow.value, budget);
|
|
111
|
+
const metrics = shared.metrics?.snow;
|
|
112
|
+
if (metrics !== undefined) {
|
|
113
|
+
metrics.count = geometry.instanceCount;
|
|
114
|
+
metrics.capacity = geometry.getAttribute("aSpawn").count;
|
|
115
|
+
}
|
|
102
116
|
});
|
|
103
117
|
return (_jsx("mesh", { geometry: geometry, material: material, frustumCulled: frustumCulled ?? !followCamera, renderOrder: renderOrder }));
|
|
104
118
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
import type { EditorVolume } from "@jgengine/core/editor/types";
|
|
3
|
+
import type { AuthoredWeatherSample } from "@jgengine/core/world/authoredWeather";
|
|
2
4
|
import { type DustFieldProps } from "./DustField.js";
|
|
3
5
|
import { type RainFieldProps } from "./RainField.js";
|
|
6
|
+
import { type RainImpactFieldProps } from "./RainImpactField.js";
|
|
4
7
|
import { type SnowFieldProps } from "./SnowField.js";
|
|
5
|
-
import { type WeatherVector } from "./weatherUniforms.js";
|
|
8
|
+
import { type WeatherVector, type WeatherParticleMetrics } from "./weatherUniforms.js";
|
|
6
9
|
/** `"mixed"` runs rain and snow together; `"dust"` is airborne particulate and composes with either via `dust`. */
|
|
7
10
|
export type WeatherLayerMode = "clear" | "rain" | "snow" | "mixed" | "dust";
|
|
8
11
|
export interface WeatherLayerProps {
|
|
@@ -11,13 +14,19 @@ export interface WeatherLayerProps {
|
|
|
11
14
|
wind?: WeatherVector;
|
|
12
15
|
lightning?: number;
|
|
13
16
|
timeScale?: number;
|
|
17
|
+
timeSeconds?: number | (() => number);
|
|
18
|
+
/** Read authored conditions at the camera from the authoritative simulation clock. */
|
|
19
|
+
sample?: (x: number, z: number) => AuthoredWeatherSample;
|
|
20
|
+
shelters?: readonly EditorVolume[];
|
|
21
|
+
metrics?: WeatherParticleMetrics;
|
|
14
22
|
rain?: Omit<RainFieldProps, "wind" | "lightning" | "timeScale"> | false;
|
|
15
23
|
snow?: Omit<SnowFieldProps, "wind" | "timeScale"> | false;
|
|
16
24
|
/** Airborne particulate. Drawn in `"dust"` mode, and alongside rain/snow whenever `dustAlways` is set. */
|
|
17
25
|
dust?: Omit<DustFieldProps, "wind" | "timeScale"> | false;
|
|
18
26
|
/** Keep dust in the air in every mode — a world that is dusty whatever else the sky is doing. */
|
|
19
27
|
dustAlways?: boolean;
|
|
28
|
+
impacts?: RainImpactFieldProps | false;
|
|
20
29
|
enabled?: boolean;
|
|
21
30
|
children?: ReactNode;
|
|
22
31
|
}
|
|
23
|
-
export declare function WeatherLayer({ mode, intensity, wind, lightning, timeScale, rain, snow, dust, dustAlways, enabled, children, }: WeatherLayerProps): import("react").JSX.Element | null;
|
|
32
|
+
export declare function WeatherLayer({ mode, intensity, wind, lightning, timeScale, timeSeconds, sample, shelters, metrics, rain, snow, dust, dustAlways, impacts, enabled, children, }: WeatherLayerProps): import("react").JSX.Element | null;
|
|
@@ -1,19 +1,43 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { DustField } from "./DustField.js";
|
|
3
3
|
import { RainField } from "./RainField.js";
|
|
4
|
+
import { RainImpactField } from "./RainImpactField.js";
|
|
4
5
|
import { SnowField } from "./SnowField.js";
|
|
5
6
|
import { WeatherUniformProvider } from "./weatherUniforms.js";
|
|
7
|
+
function clearMetrics(metrics, layer) {
|
|
8
|
+
metrics[layer].count = 0;
|
|
9
|
+
metrics[layer].capacity = 0;
|
|
10
|
+
if (layer === "impacts") {
|
|
11
|
+
metrics.impacts.heightQueries = 0;
|
|
12
|
+
metrics.impacts.exposureQueries = 0;
|
|
13
|
+
metrics.impacts.queryBudget = 0;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
6
16
|
function resolveLayerDensity(value, fallback, intensity) {
|
|
7
17
|
return (value ?? fallback) * intensity;
|
|
8
18
|
}
|
|
9
|
-
export function WeatherLayer({ mode = "clear", intensity = 1, wind, lightning, timeScale, rain, snow, dust, dustAlways = false, enabled = true, children, }) {
|
|
10
|
-
if (!enabled)
|
|
19
|
+
export function WeatherLayer({ mode = "clear", intensity = 1, wind, lightning, timeScale, timeSeconds, sample, shelters, metrics, rain, snow, dust, dustAlways = false, impacts, enabled = true, children, }) {
|
|
20
|
+
if (!enabled) {
|
|
21
|
+
if (metrics !== undefined)
|
|
22
|
+
for (const layer of ["rain", "snow", "dust", "impacts"])
|
|
23
|
+
clearMetrics(metrics, layer);
|
|
11
24
|
return null;
|
|
25
|
+
}
|
|
12
26
|
const rainProps = rain === false ? null : (rain ?? {});
|
|
13
27
|
const snowProps = snow === false ? null : (snow ?? {});
|
|
14
|
-
const showRain = rainProps !== null && (mode === "rain" || mode === "mixed");
|
|
15
|
-
const showSnow = snowProps !== null && (mode === "snow" || mode === "mixed");
|
|
28
|
+
const showRain = rainProps !== null && (sample !== undefined || mode === "rain" || mode === "mixed");
|
|
29
|
+
const showSnow = snowProps !== null && (sample !== undefined || mode === "snow" || mode === "mixed");
|
|
16
30
|
const dustProps = dust === false ? null : (dust ?? {});
|
|
17
|
-
const showDust = dustProps !== null && (mode === "dust" || dustAlways);
|
|
18
|
-
|
|
31
|
+
const showDust = dustProps !== null && (sample !== undefined || mode === "dust" || dustAlways);
|
|
32
|
+
if (metrics !== undefined) {
|
|
33
|
+
if (!showRain)
|
|
34
|
+
clearMetrics(metrics, "rain");
|
|
35
|
+
if (!showSnow)
|
|
36
|
+
clearMetrics(metrics, "snow");
|
|
37
|
+
if (!showDust)
|
|
38
|
+
clearMetrics(metrics, "dust");
|
|
39
|
+
if (!showRain || impacts === undefined || impacts === false)
|
|
40
|
+
clearMetrics(metrics, "impacts");
|
|
41
|
+
}
|
|
42
|
+
return (_jsxs(WeatherUniformProvider, { wind: wind, lightning: lightning, timeScale: timeScale, timeSeconds: timeSeconds, sample: sample, shelters: shelters, metrics: metrics, children: [showRain ? (_jsx(RainField, { ...rainProps, density: resolveLayerDensity(rainProps.density, 0.45, intensity) })) : null, showSnow ? (_jsx(SnowField, { ...snowProps, density: resolveLayerDensity(snowProps.density, 0.5, intensity) })) : null, showDust ? (_jsx(DustField, { ...dustProps, density: resolveLayerDensity(dustProps.density, 0.4, intensity) })) : null, showRain && impacts !== undefined && impacts !== false ? _jsx(RainImpactField, { ...impacts }) : null, children] }));
|
|
19
43
|
}
|
package/dist/weather/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { DustField, type DustFieldProps } from "./DustField.js";
|
|
2
|
+
export { AuthoredWeatherLayer, type AuthoredWeatherLayerProps } from "./AuthoredWeatherLayer.js";
|
|
3
|
+
export { RainImpactField, type RainImpactFieldProps } from "./RainImpactField.js";
|
|
2
4
|
export { FireSpreadLayer, type FireSpreadLayerProps } from "./FireSpreadLayer.js";
|
|
3
5
|
export { LightningStrike, type LightningStrikeProps } from "./LightningStrike.js";
|
|
4
6
|
export { RainField, type RainFieldProps } from "./RainField.js";
|
package/dist/weather/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { DustField } from "./DustField.js";
|
|
2
|
+
export { AuthoredWeatherLayer } from "./AuthoredWeatherLayer.js";
|
|
3
|
+
export { RainImpactField } from "./RainImpactField.js";
|
|
2
4
|
export { FireSpreadLayer } from "./FireSpreadLayer.js";
|
|
3
5
|
export { LightningStrike } from "./LightningStrike.js";
|
|
4
6
|
export { RainField } from "./RainField.js";
|
|
@@ -16,3 +16,5 @@ export interface WeatherSeedAttributes {
|
|
|
16
16
|
}
|
|
17
17
|
/** @internal */
|
|
18
18
|
export declare function createWeatherSeedAttributes(maxCount: number, seed: number): WeatherSeedAttributes;
|
|
19
|
+
/** @internal */
|
|
20
|
+
export declare function lightningStrikeEnvelope(timeSeconds: number, startedAt: number, duration: number, seed: number): number;
|
|
@@ -40,3 +40,11 @@ export function createWeatherSeedAttributes(maxCount, seed) {
|
|
|
40
40
|
}
|
|
41
41
|
return { spawn, drift };
|
|
42
42
|
}
|
|
43
|
+
/** @internal */
|
|
44
|
+
export function lightningStrikeEnvelope(timeSeconds, startedAt, duration, seed) {
|
|
45
|
+
const elapsed = timeSeconds - startedAt;
|
|
46
|
+
if (elapsed < 0 || duration <= 0 || elapsed >= duration)
|
|
47
|
+
return 0;
|
|
48
|
+
const flicker = 0.62 + (Math.sin(elapsed * 97 + seed) * 0.5 + 0.5) * 0.38;
|
|
49
|
+
return (1 - elapsed / duration) * flicker;
|
|
50
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type { EditorVolume } from "@jgengine/core/editor/types";
|
|
3
|
+
/** Cosmetic roof clipping is bounded independently of authoritative shelter queries. */
|
|
4
|
+
export declare const WEATHER_SHELTER_BUDGET = 32;
|
|
5
|
+
/** Reusable buffers for the nearest authored shelters submitted to weather shaders. */
|
|
6
|
+
export interface WeatherShelterUniforms {
|
|
7
|
+
shelterCenters: THREE.IUniform<THREE.Vector4[]>;
|
|
8
|
+
shelterShapes: THREE.IUniform<THREE.Vector4[]>;
|
|
9
|
+
shelterCount: THREE.IUniform<number>;
|
|
10
|
+
shelterIndices: Int32Array;
|
|
11
|
+
shelterDistances: Float64Array;
|
|
12
|
+
}
|
|
13
|
+
/** @internal */
|
|
14
|
+
export declare function createWeatherShelterUniforms(): WeatherShelterUniforms;
|
|
15
|
+
/** @internal */
|
|
16
|
+
export declare function updateWeatherShelters(uniforms: WeatherShelterUniforms, shelters: readonly EditorVolume[], x: number, y: number, z: number): void;
|
|
17
|
+
/** @internal */
|
|
18
|
+
export declare const WEATHER_SHELTER_SHADER = "\n uniform vec4 uShelterCenters[32];\n uniform vec4 uShelterShapes[32];\n uniform int uShelterCount;\n float weatherExposure(vec3 point) {\n float exposure = 1.0;\n for (int i = 0; i < 32; i++) {\n if (i >= uShelterCount) break;\n vec4 center = uShelterCenters[i];\n vec4 shape = uShelterShapes[i];\n vec3 delta = abs(point - center.xyz);\n bool inside = center.w < 0.5 ? all(lessThanEqual(delta, shape.xyz)) : center.w < 1.5 ? length(delta) <= shape.x : length(delta.xz) <= shape.x && delta.y <= shape.y;\n if (inside) exposure = min(exposure, shape.w);\n }\n return exposure;\n }\n";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
/** Cosmetic roof clipping is bounded independently of authoritative shelter queries. */
|
|
3
|
+
export const WEATHER_SHELTER_BUDGET = 32;
|
|
4
|
+
/** @internal */
|
|
5
|
+
export function createWeatherShelterUniforms() {
|
|
6
|
+
return {
|
|
7
|
+
shelterCenters: { value: Array.from({ length: WEATHER_SHELTER_BUDGET }, () => new THREE.Vector4()) },
|
|
8
|
+
shelterShapes: { value: Array.from({ length: WEATHER_SHELTER_BUDGET }, () => new THREE.Vector4()) },
|
|
9
|
+
shelterCount: { value: 0 },
|
|
10
|
+
shelterIndices: new Int32Array(WEATHER_SHELTER_BUDGET),
|
|
11
|
+
shelterDistances: new Float64Array(WEATHER_SHELTER_BUDGET),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** @internal */
|
|
15
|
+
export function updateWeatherShelters(uniforms, shelters, x, y, z) {
|
|
16
|
+
const indices = uniforms.shelterIndices;
|
|
17
|
+
const distances = uniforms.shelterDistances;
|
|
18
|
+
distances.fill(Infinity);
|
|
19
|
+
indices.fill(-1);
|
|
20
|
+
for (let index = 0; index < shelters.length; index += 1) {
|
|
21
|
+
const shelter = shelters[index];
|
|
22
|
+
if (shelter.kind !== "shelter")
|
|
23
|
+
continue;
|
|
24
|
+
const hx = shelter.shape === "box" ? shelter.halfExtents?.x ?? 0 : shelter.radius ?? 0;
|
|
25
|
+
const hy = shelter.shape === "box" ? shelter.halfExtents?.y ?? 0 : shelter.shape === "sphere" ? shelter.radius ?? 0 : (shelter.height ?? 0) * 0.5;
|
|
26
|
+
const hz = shelter.shape === "box" ? shelter.halfExtents?.z ?? 0 : shelter.radius ?? 0;
|
|
27
|
+
const dx = Math.max(0, Math.abs(x - shelter.center.x) - hx);
|
|
28
|
+
const dy = Math.max(0, Math.abs(y - shelter.center.y) - hy);
|
|
29
|
+
const dz = Math.max(0, Math.abs(z - shelter.center.z) - hz);
|
|
30
|
+
const distance = dx * dx + dy * dy + dz * dz;
|
|
31
|
+
for (let slot = 0; slot < WEATHER_SHELTER_BUDGET; slot += 1) {
|
|
32
|
+
if (distance >= distances[slot])
|
|
33
|
+
continue;
|
|
34
|
+
for (let move = WEATHER_SHELTER_BUDGET - 1; move > slot; move -= 1) {
|
|
35
|
+
distances[move] = distances[move - 1];
|
|
36
|
+
indices[move] = indices[move - 1];
|
|
37
|
+
}
|
|
38
|
+
distances[slot] = distance;
|
|
39
|
+
indices[slot] = index;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
let count = 0;
|
|
44
|
+
for (let slot = 0; slot < WEATHER_SHELTER_BUDGET; slot += 1) {
|
|
45
|
+
const index = indices[slot];
|
|
46
|
+
if (index < 0)
|
|
47
|
+
break;
|
|
48
|
+
const shelter = shelters[index];
|
|
49
|
+
const center = shelter.center;
|
|
50
|
+
uniforms.shelterCenters.value[slot].set(center.x, center.y, center.z, shelter.shape === "box" ? 0 : shelter.shape === "sphere" ? 1 : 2);
|
|
51
|
+
const exposure = typeof shelter.meta?.exposure === "number" ? Math.max(0, Math.min(1, shelter.meta.exposure)) : 0;
|
|
52
|
+
const extents = shelter.halfExtents;
|
|
53
|
+
uniforms.shelterShapes.value[slot].set(shelter.shape === "box" ? extents?.x ?? 0 : shelter.radius ?? 0, shelter.shape === "box" ? extents?.y ?? 0 : (shelter.height ?? 0) * 0.5, shelter.shape === "box" ? extents?.z ?? 0 : 0, exposure);
|
|
54
|
+
count += 1;
|
|
55
|
+
}
|
|
56
|
+
uniforms.shelterCount.value = count;
|
|
57
|
+
}
|
|
58
|
+
/** @internal */
|
|
59
|
+
export const WEATHER_SHELTER_SHADER = `
|
|
60
|
+
uniform vec4 uShelterCenters[32];
|
|
61
|
+
uniform vec4 uShelterShapes[32];
|
|
62
|
+
uniform int uShelterCount;
|
|
63
|
+
float weatherExposure(vec3 point) {
|
|
64
|
+
float exposure = 1.0;
|
|
65
|
+
for (int i = 0; i < 32; i++) {
|
|
66
|
+
if (i >= uShelterCount) break;
|
|
67
|
+
vec4 center = uShelterCenters[i];
|
|
68
|
+
vec4 shape = uShelterShapes[i];
|
|
69
|
+
vec3 delta = abs(point - center.xyz);
|
|
70
|
+
bool inside = center.w < 0.5 ? all(lessThanEqual(delta, shape.xyz)) : center.w < 1.5 ? length(delta) <= shape.x : length(delta.xz) <= shape.x && delta.y <= shape.y;
|
|
71
|
+
if (inside) exposure = min(exposure, shape.w);
|
|
72
|
+
}
|
|
73
|
+
return exposure;
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
@@ -1,19 +1,58 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import * as THREE from "three";
|
|
3
|
+
import type { EditorVolume } from "@jgengine/core/editor/types";
|
|
4
|
+
import { type WeatherShelterUniforms } from "./weatherShelter.js";
|
|
5
|
+
import type { AuthoredWeatherSample } from "@jgengine/core/world/authoredWeather";
|
|
3
6
|
export type WeatherVector = readonly [number, number, number];
|
|
4
|
-
|
|
7
|
+
/** Actual procedural weather submissions and per-frame impact query work. */
|
|
8
|
+
export interface WeatherParticleMetrics {
|
|
9
|
+
rain: {
|
|
10
|
+
count: number;
|
|
11
|
+
capacity: number;
|
|
12
|
+
};
|
|
13
|
+
snow: {
|
|
14
|
+
count: number;
|
|
15
|
+
capacity: number;
|
|
16
|
+
};
|
|
17
|
+
dust: {
|
|
18
|
+
count: number;
|
|
19
|
+
capacity: number;
|
|
20
|
+
};
|
|
21
|
+
impacts: {
|
|
22
|
+
count: number;
|
|
23
|
+
capacity: number;
|
|
24
|
+
heightQueries: number;
|
|
25
|
+
exposureQueries: number;
|
|
26
|
+
queryBudget: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export interface WeatherUniformSet extends WeatherShelterUniforms {
|
|
5
30
|
time: THREE.IUniform<number>;
|
|
6
31
|
wind: THREE.IUniform<THREE.Vector3>;
|
|
7
32
|
lightning: THREE.IUniform<number>;
|
|
33
|
+
rain: THREE.IUniform<number>;
|
|
34
|
+
snow: THREE.IUniform<number>;
|
|
35
|
+
dust: THREE.IUniform<number>;
|
|
36
|
+
controlsWind: boolean;
|
|
37
|
+
metrics?: WeatherParticleMetrics;
|
|
8
38
|
}
|
|
9
39
|
export interface WeatherUniformOptions {
|
|
10
40
|
wind?: WeatherVector;
|
|
11
41
|
lightning?: number;
|
|
12
42
|
timeScale?: number;
|
|
43
|
+
/** Absolute authoritative simulation seconds; no render delta is accumulated when supplied. */
|
|
44
|
+
timeSeconds?: number | (() => number);
|
|
45
|
+
sample?: (x: number, z: number) => AuthoredWeatherSample;
|
|
46
|
+
/** The closest 32 authored shelter volumes clip cosmetic particles. */
|
|
47
|
+
shelters?: readonly EditorVolume[];
|
|
48
|
+
/** Opt-in counters read from submitted geometry and actual query calls. */
|
|
49
|
+
metrics?: WeatherParticleMetrics;
|
|
13
50
|
}
|
|
14
51
|
/** @internal */
|
|
15
52
|
export declare function createWeatherUniformSet(options?: WeatherUniformOptions): WeatherUniformSet;
|
|
16
53
|
/** @internal */
|
|
54
|
+
export declare function updateWeatherUniformSet(uniforms: WeatherUniformSet, options: WeatherUniformOptions, delta: number, x?: number, z?: number, y?: number): void;
|
|
55
|
+
/** @internal */
|
|
17
56
|
export declare function WeatherUniformProvider({ children, ...options }: WeatherUniformOptions & {
|
|
18
57
|
children: ReactNode;
|
|
19
58
|
}): import("react").JSX.Element;
|