@jgengine/shell 0.10.0 → 0.12.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 +39 -2
- package/LICENSE +203 -661
- package/README.md +1 -1
- package/dist/CombatPresentation.d.ts +5 -0
- package/dist/CombatPresentation.js +8 -0
- package/dist/GameHost.d.ts +25 -1
- package/dist/GameHost.js +60 -4
- package/dist/GamePhaseStamp.d.ts +1 -0
- package/dist/GamePhaseStamp.js +9 -0
- package/dist/GamePlayer.d.ts +5 -0
- package/dist/GamePlayer.js +5 -0
- package/dist/GamePlayerShell.d.ts +5 -12
- package/dist/GamePlayerShell.js +67 -1332
- package/dist/GameUiPreview.js +3 -2
- package/dist/Shell3dPresentation.d.ts +63 -0
- package/dist/Shell3dPresentation.js +302 -0
- package/dist/ShellChrome.d.ts +51 -0
- package/dist/ShellChrome.js +51 -0
- package/dist/ShellHudPresentation.d.ts +41 -0
- package/dist/ShellHudPresentation.js +22 -0
- package/dist/audio/audioEngine.d.ts +4 -0
- package/dist/audio/audioEngine.js +128 -21
- package/dist/audio/audioWire.d.ts +40 -0
- package/dist/audio/audioWire.js +62 -0
- package/dist/audio/loopParams.d.ts +20 -0
- package/dist/audio/loopParams.js +28 -0
- package/dist/behaviour.d.ts +2 -1
- package/dist/behaviour.js +2 -1
- package/dist/behaviourAttach.d.ts +1 -0
- package/dist/behaviourAttach.js +1 -0
- package/dist/behaviourDriver.d.ts +1 -0
- package/dist/behaviourDriver.js +1 -0
- package/dist/boundActionDispatch.d.ts +13 -0
- package/dist/boundActionDispatch.js +37 -0
- package/dist/camera/GameCameraRig.d.ts +5 -2
- package/dist/camera/GameCameraRig.js +8 -3
- package/dist/camera/GameFirstPersonCamera.d.ts +9 -2
- package/dist/camera/GameFirstPersonCamera.js +8 -3
- package/dist/camera/GameInspectionCamera.d.ts +1 -1
- package/dist/camera/GameInspectionCamera.js +2 -2
- package/dist/camera/GameOrbitCamera.js +24 -9
- package/dist/camera/cameraBlendMath.d.ts +3 -0
- package/dist/camera/cameraBlendMath.js +3 -0
- package/dist/camera/cameraRigs.d.ts +3 -1
- package/dist/camera/cameraRigs.js +21 -3
- package/dist/camera/fovPreference.d.ts +6 -1
- package/dist/camera/fovPreference.js +6 -1
- package/dist/camera/index.d.ts +4 -7
- package/dist/camera/index.js +4 -7
- package/dist/camera/inspectionCameraMath.d.ts +7 -2
- package/dist/camera/inspectionCameraMath.js +7 -2
- package/dist/camera/orbitCameraMath.d.ts +36 -6
- package/dist/camera/orbitCameraMath.js +56 -6
- package/dist/camera/rigMath.d.ts +114 -24
- package/dist/camera/rigMath.js +129 -24
- package/dist/camera/rigResolve.d.ts +4 -2
- package/dist/camera/rigResolve.js +4 -2
- package/dist/camera/shakeChannel.d.ts +6 -2
- package/dist/camera/shakeChannel.js +6 -2
- package/dist/camera/shakeChannelMath.d.ts +1 -0
- package/dist/camera/shakeChannelMath.js +1 -0
- package/dist/commandSink.d.ts +8 -3
- package/dist/commandSink.js +8 -3
- package/dist/defineGame.d.ts +15 -1
- package/dist/defineGame.js +50 -9
- package/dist/devtools/ColPanel.d.ts +1 -0
- package/dist/devtools/ColPanel.js +14 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +11 -7
- package/dist/devtools/DevtoolsOverlay.js +47 -462
- package/dist/devtools/KeysPanel.d.ts +4 -0
- package/dist/devtools/KeysPanel.js +31 -0
- package/dist/devtools/LogsPanel.d.ts +1 -0
- package/dist/devtools/LogsPanel.js +12 -0
- package/dist/devtools/NetPanel.d.ts +4 -0
- package/dist/devtools/NetPanel.js +10 -0
- package/dist/devtools/PerfPanel.d.ts +4 -0
- package/dist/devtools/PerfPanel.js +53 -0
- package/dist/devtools/TunePanel.d.ts +3 -0
- package/dist/devtools/TunePanel.js +309 -0
- package/dist/devtools/agentBridge.d.ts +41 -0
- package/dist/devtools/agentBridge.js +266 -0
- package/dist/devtools/collisionDebug.d.ts +6 -0
- package/dist/devtools/collisionDebug.js +6 -0
- package/dist/devtools/collisionDebugMath.d.ts +8 -2
- package/dist/devtools/collisionDebugMath.js +24 -5
- package/dist/devtools/devtoolsOverrides.d.ts +6 -0
- package/dist/devtools/devtoolsOverrides.js +48 -0
- package/dist/devtools/panelAtoms.d.ts +9 -0
- package/dist/devtools/panelAtoms.js +10 -0
- package/dist/devtools/perfDiagnose.d.ts +2 -0
- package/dist/devtools/perfDiagnose.js +43 -0
- package/dist/diagnostics/RuntimeDiagnostics.d.ts +36 -0
- package/dist/diagnostics/RuntimeDiagnostics.js +80 -0
- package/dist/drivers/FrameDriver.d.ts +37 -0
- package/dist/drivers/FrameDriver.js +232 -0
- package/dist/drivers/HudOnlyDriver.d.ts +16 -0
- package/dist/drivers/HudOnlyDriver.js +60 -0
- package/dist/environment/Daylight.d.ts +23 -7
- package/dist/environment/Daylight.js +106 -13
- package/dist/environment/EnvironmentScene.js +44 -9
- package/dist/environment/VolumetricClouds.d.ts +17 -0
- package/dist/environment/VolumetricClouds.js +202 -0
- package/dist/environment/daylightCycle.d.ts +3 -1
- package/dist/environment/daylightCycle.js +3 -1
- package/dist/environment/groundPadMath.d.ts +3 -0
- package/dist/environment/groundPadMath.js +3 -0
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/environment/skyLightingPolicy.d.ts +2 -0
- package/dist/environment/skyLightingPolicy.js +2 -0
- package/dist/environment/terrainGroundColor.d.ts +9 -0
- package/dist/environment/terrainGroundColor.js +22 -0
- package/dist/gameKit.d.ts +27 -0
- package/dist/gameKit.js +29 -0
- package/dist/hotbarActions.d.ts +26 -0
- package/dist/hotbarActions.js +27 -0
- package/dist/inputSink.d.ts +16 -4
- package/dist/inputSink.js +57 -7
- package/dist/map/MapMarkerBeacons.d.ts +3 -2
- package/dist/materialOverride.d.ts +10 -0
- package/dist/materialOverride.js +15 -3
- package/dist/multiplayer.js +8 -2
- package/dist/postfx/PostProcessing.d.ts +9 -1
- package/dist/postfx/PostProcessing.js +56 -11
- package/dist/postfx/gradeShader.js +9 -1
- package/dist/presentationResolve.d.ts +35 -0
- package/dist/presentationResolve.js +51 -0
- package/dist/registry.d.ts +4 -2
- package/dist/render/EnvironmentLighting.d.ts +10 -0
- package/dist/render/EnvironmentLighting.js +34 -0
- package/dist/render/SceneLighting.d.ts +7 -0
- package/dist/render/SceneLighting.js +18 -0
- package/dist/render/SceneModels.d.ts +14 -0
- package/dist/render/SceneModels.js +337 -0
- package/dist/render/assetBase.d.ts +17 -0
- package/dist/render/assetBase.js +35 -0
- package/dist/render/defaultSurface.d.ts +29 -0
- package/dist/render/defaultSurface.js +101 -0
- package/dist/render/modelLoad.d.ts +24 -0
- package/dist/render/modelLoad.js +76 -0
- package/dist/render/modelRender.d.ts +11 -0
- package/dist/render/modelRender.js +15 -0
- package/dist/render/resolveModel.d.ts +54 -3
- package/dist/render/resolveModel.js +116 -4
- package/dist/render/useEntityRenderCues.d.ts +13 -0
- package/dist/render/useEntityRenderCues.js +62 -0
- package/dist/scatter/InstancedScatter.d.ts +24 -0
- package/dist/scatter/InstancedScatter.js +135 -0
- package/dist/scatter/scatterModels.d.ts +25 -0
- package/dist/scatter/scatterModels.js +61 -0
- package/dist/scatter/scatterProxies.d.ts +6 -0
- package/dist/scatter/scatterProxies.js +118 -0
- package/dist/scatter.d.ts +1 -0
- package/dist/scatter.js +1 -0
- package/dist/scene/AuthoredScene.d.ts +84 -0
- package/dist/scene/AuthoredScene.js +178 -0
- package/dist/scene/CityRenderer.d.ts +2 -0
- package/dist/scene/CityRenderer.js +1031 -0
- package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
- package/dist/scene/GeneratedAssetRenderer.js +59 -0
- package/dist/scene/GrassFieldRenderer.d.ts +2 -0
- package/dist/scene/GrassFieldRenderer.js +73 -0
- package/dist/scene/PoleLineRenderer.d.ts +2 -0
- package/dist/scene/PoleLineRenderer.js +287 -0
- package/dist/scene/SoilPatchRenderer.d.ts +2 -0
- package/dist/scene/SoilPatchRenderer.js +84 -0
- package/dist/scene/StudioStage.d.ts +37 -0
- package/dist/scene/StudioStage.js +42 -0
- package/dist/scene/WaterRenderer.d.ts +2 -0
- package/dist/scene/WaterRenderer.js +61 -0
- package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
- package/dist/scene/builtinSceneKindRenderers.js +26 -0
- package/dist/scene/cityWindowMaterial.d.ts +16 -0
- package/dist/scene/cityWindowMaterial.js +81 -0
- package/dist/scene/sceneKindRenderers.d.ts +30 -0
- package/dist/scene/sceneKindRenderers.js +17 -0
- package/dist/scene.d.ts +1 -0
- package/dist/scene.js +1 -0
- package/dist/settings/appliedSettings.d.ts +7 -1
- package/dist/settings/appliedSettings.js +13 -2
- package/dist/settings/settingsController.d.ts +2 -0
- package/dist/settings/settingsController.js +22 -1
- package/dist/shellConstants.d.ts +4 -0
- package/dist/shellConstants.js +4 -0
- package/dist/shellMovement.d.ts +8 -0
- package/dist/shellMovement.js +11 -0
- package/dist/shellPointer.d.ts +20 -0
- package/dist/shellPointer.js +28 -0
- package/dist/structures/PlacementGhost.d.ts +4 -0
- package/dist/structures/PlacementGhost.js +4 -0
- package/dist/structures/TransformGizmo.d.ts +53 -0
- package/dist/structures/TransformGizmo.js +59 -0
- package/dist/structures/index.d.ts +1 -0
- package/dist/structures/index.js +1 -0
- package/dist/terrain/GrassField.d.ts +11 -3
- package/dist/terrain/GrassField.js +16 -6
- package/dist/terrain/TerraformBrushCursor.js +3 -0
- package/dist/terrain/grassBudget.d.ts +11 -2
- package/dist/terrain/grassBudget.js +20 -4
- package/dist/terrain/grassGeometry.d.ts +35 -0
- package/dist/terrain/grassGeometry.js +85 -20
- package/dist/terrain/grassMaterial.d.ts +20 -0
- package/dist/terrain/grassMaterial.js +68 -13
- package/dist/terrain/heightfieldGeometry.d.ts +34 -0
- package/dist/terrain/heightfieldGeometry.js +109 -0
- package/dist/terrain/index.d.ts +1 -0
- package/dist/terrain/index.js +1 -0
- package/dist/terrain/random.d.ts +3 -0
- package/dist/terrain/random.js +3 -0
- package/dist/terrain/soilPatchMaterial.d.ts +23 -0
- package/dist/terrain/soilPatchMaterial.js +147 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +19 -2
- package/dist/terrain/terrainDetailMaterial.js +87 -7
- package/dist/terrain/terrainMath.d.ts +8 -1
- package/dist/terrain/terrainMath.js +8 -1
- package/dist/touch/TouchControlsOverlay.d.ts +9 -5
- package/dist/touch/TouchControlsOverlay.js +201 -22
- package/dist/useShellMultiplayerSync.d.ts +9 -0
- package/dist/useShellMultiplayerSync.js +98 -0
- package/dist/visibility/CullingProvider.js +1 -1
- package/dist/water/Ocean.d.ts +7 -1
- package/dist/water/Ocean.js +16 -3
- package/dist/water/OceanConfig.d.ts +11 -0
- package/dist/water/OceanConfig.js +8 -0
- package/dist/water/OceanMaterial.d.ts +12 -0
- package/dist/water/OceanMaterial.js +9 -0
- package/dist/water/OceanShader.d.ts +2 -2
- package/dist/water/OceanShader.js +78 -10
- package/dist/weather/fireSpreadPose.d.ts +1 -0
- package/dist/weather/fireSpreadPose.js +1 -0
- package/dist/weather/weatherGeometry.d.ts +1 -0
- package/dist/weather/weatherGeometry.js +1 -0
- package/dist/weather/weatherMath.d.ts +3 -0
- package/dist/weather/weatherMath.js +3 -0
- package/dist/weather/weatherUniforms.d.ts +3 -0
- package/dist/weather/weatherUniforms.js +3 -0
- package/dist/world/PlaceScene.d.ts +11 -0
- package/dist/world/PlaceScene.js +27 -0
- package/dist/world/RetainedVfx.d.ts +10 -0
- package/dist/world/RetainedVfx.js +136 -0
- package/dist/world/WorldHud.d.ts +49 -2
- package/dist/world/WorldHud.js +87 -2
- package/dist/world/WorldScene.d.ts +21 -0
- package/dist/world/WorldScene.js +186 -0
- package/dist/world/WorldVfx.d.ts +9 -0
- package/dist/world/WorldVfx.js +184 -0
- package/dist/world/entityPose.d.ts +2 -0
- package/dist/world/entityPose.js +2 -0
- package/dist/world/floatTextStyle.d.ts +1 -0
- package/dist/world/floatTextStyle.js +1 -0
- package/dist/world/telegraphPulse.d.ts +1 -0
- package/dist/world/telegraphPulse.js +1 -0
- package/dist/world/worldBarSamples.d.ts +25 -0
- package/dist/world/worldBarSamples.js +38 -0
- package/dist/worldSky.d.ts +11 -0
- package/dist/worldSky.js +21 -0
- package/dist/worldSync.d.ts +2 -1
- package/dist/worldSync.js +2 -1
- package/package.json +6 -6
- package/skills/game-design/SKILL.md +28 -0
- package/skills/game-design/agents/openai.yaml +4 -0
- package/skills/game-design/references/application-playbook.md +170 -0
- package/skills/game-design/references/field-guide.md +288 -0
- package/skills/jgengine/SKILL.md +54 -0
- package/skills/jgengine/api.md +527 -0
- package/skills/jgengine/capabilities.md +131 -0
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +41 -0
- package/skills/jgengine-ui/api.md +2007 -0
- package/skills/jgengine-ui/capabilities.md +211 -0
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +513 -0
- package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
- package/skills/jgengine-world/SKILL.md +51 -0
- package/skills/jgengine-world/api.md +3056 -0
- package/skills/jgengine-world/capabilities.md +476 -0
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +261 -0
- package/skills/level-design/SKILL.md +54 -0
- package/skills/level-design/agents/openai.yaml +4 -0
- package/skills/level-design/references/application-playbook.md +192 -0
- package/skills/level-design/references/field-guide.md +337 -0
- package/skills/level-design/references/live-editor-loop.md +179 -0
- package/dist/cartridge.d.ts +0 -145
- package/dist/cartridge.js +0 -246
- package/llms.txt +0 -2761
|
@@ -2,7 +2,10 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { useFrame } from "@react-three/fiber";
|
|
3
3
|
import { useEffect, useMemo, useRef } from "react";
|
|
4
4
|
import * as THREE from "three";
|
|
5
|
+
import { createBiomeFogSampler, createBiomeSkySampler } from "@jgengine/core/world/terrain";
|
|
6
|
+
import { resolveVolumetricClouds } from "@jgengine/core/world/volumetricClouds";
|
|
5
7
|
import { daylightStateAt, SKY_PRESET_DAY_FRACTION } from "./daylightCycle.js";
|
|
8
|
+
import { VolumetricClouds } from "./VolumetricClouds.js";
|
|
6
9
|
function normalizeVec3(v) {
|
|
7
10
|
const out = new THREE.Vector3(v[0], v[1], v[2]);
|
|
8
11
|
return out.lengthSq() === 0 ? new THREE.Vector3(0, 1, 0) : out.normalize();
|
|
@@ -32,7 +35,9 @@ function ShadowCastingSun({ position, intensity, color, }) {
|
|
|
32
35
|
});
|
|
33
36
|
return (_jsx("directionalLight", { ref: ref, position: [position[0], position[1], position[2]], intensity: intensity, color: color, castShadow: true, "shadow-mapSize-width": 2048, "shadow-mapSize-height": 2048, "shadow-camera-left": -90, "shadow-camera-right": 90, "shadow-camera-top": 90, "shadow-camera-bottom": -90, "shadow-camera-near": 10, "shadow-camera-far": 520, "shadow-bias": -0.0004, "shadow-normalBias": 0.02 }));
|
|
34
37
|
}
|
|
35
|
-
|
|
38
|
+
/** @internal */
|
|
39
|
+
export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, sunDirection, sunColor = "#fff4d6", sunIntensity = 1, hazeStrength = 0.5, sunGlowStrength = 1, materialRef, } = {}) {
|
|
40
|
+
const meshRef = useRef(null);
|
|
36
41
|
const material = useMemo(() => {
|
|
37
42
|
return new THREE.ShaderMaterial({
|
|
38
43
|
uniforms: {
|
|
@@ -43,6 +48,8 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
43
48
|
uSunColor: { value: new THREE.Color(sunColor) },
|
|
44
49
|
uSunDirection: { value: sunDirection === undefined ? new THREE.Vector3(0, 1, 0) : normalizeVec3(sunDirection) },
|
|
45
50
|
uSunIntensity: { value: sunDirection === undefined ? 0 : sunIntensity },
|
|
51
|
+
uHazeStrength: { value: hazeStrength },
|
|
52
|
+
uSunGlow: { value: sunGlowStrength },
|
|
46
53
|
},
|
|
47
54
|
vertexShader: `
|
|
48
55
|
varying vec3 vWorldPosition;
|
|
@@ -58,6 +65,8 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
58
65
|
uniform vec3 uSunColor;
|
|
59
66
|
uniform vec3 uSunDirection;
|
|
60
67
|
uniform float uSunIntensity;
|
|
68
|
+
uniform float uHazeStrength;
|
|
69
|
+
uniform float uSunGlow;
|
|
61
70
|
uniform float offset;
|
|
62
71
|
uniform float exponent;
|
|
63
72
|
varying vec3 vWorldPosition;
|
|
@@ -72,7 +81,7 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
72
81
|
vec3 dir = normalize(vWorldPosition + vec3(0.0, offset, 0.0));
|
|
73
82
|
float h = max(dir.y, 0.0);
|
|
74
83
|
vec3 col = mix(bottomColor, topColor, pow(h, exponent));
|
|
75
|
-
col = mix(col, bottomColor * 1.12, pow(1.0 - h, 6.0) *
|
|
84
|
+
col = mix(col, bottomColor * 1.12, pow(1.0 - h, 6.0) * uHazeStrength);
|
|
76
85
|
vec3 vd = normalize(vWorldPosition);
|
|
77
86
|
vec2 cuv = vd.xz / (max(vd.y, 0.06) + 0.15) * 1.2;
|
|
78
87
|
float clouds = smoothstep(0.52, 0.82, sFbm(cuv));
|
|
@@ -80,7 +89,7 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
80
89
|
col = mix(col, mix(topColor, vec3(1.0), 0.6), clouds * band * 0.35);
|
|
81
90
|
float sd = max(dot(vd, normalize(uSunDirection)), 0.0);
|
|
82
91
|
float glow = pow(sd, 8.0) * 0.35 + pow(sd, 128.0) * 2.6;
|
|
83
|
-
col += uSunColor * glow * uSunIntensity;
|
|
92
|
+
col += uSunColor * glow * uSunIntensity * uSunGlow;
|
|
84
93
|
gl_FragColor = vec4(col, 1.0);
|
|
85
94
|
}
|
|
86
95
|
`,
|
|
@@ -88,7 +97,7 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
88
97
|
depthWrite: false,
|
|
89
98
|
fog: false,
|
|
90
99
|
});
|
|
91
|
-
}, [topColor, horizonColor, offset, exponent, sunColor, sunDirection, sunIntensity]);
|
|
100
|
+
}, [topColor, horizonColor, offset, exponent, sunColor, sunDirection, sunIntensity, hazeStrength, sunGlowStrength]);
|
|
92
101
|
useEffect(() => {
|
|
93
102
|
if (materialRef !== undefined)
|
|
94
103
|
materialRef.current = material;
|
|
@@ -98,27 +107,111 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
98
107
|
materialRef.current = null;
|
|
99
108
|
};
|
|
100
109
|
}, [material, materialRef]);
|
|
101
|
-
|
|
110
|
+
useFrame((state) => {
|
|
111
|
+
const mesh = meshRef.current;
|
|
112
|
+
if (mesh === null)
|
|
113
|
+
return;
|
|
114
|
+
mesh.position.x = state.camera.position.x;
|
|
115
|
+
mesh.position.z = state.camera.position.z;
|
|
116
|
+
});
|
|
117
|
+
return (_jsx("mesh", { ref: meshRef, material: material, renderOrder: -1, frustumCulled: false, children: _jsx("sphereGeometry", { args: [radius, 32, 16] }) }));
|
|
102
118
|
}
|
|
103
|
-
|
|
119
|
+
/** @internal */
|
|
120
|
+
export function Daylight({ sky, fog, sun, ambient, lights = true, clouds } = {}) {
|
|
104
121
|
const sunPosition = sun?.position ?? [120, 160, 70];
|
|
105
|
-
return (_jsxs(_Fragment, { children: [sky === false ? null : (_jsx(SkyDome, { ...(sky ?? {}), sunDirection: sunPosition, sunColor: sun?.color ?? SUN_COLOR, sunIntensity: sun?.intensity ?? 1 })), fog === false ? null : (_jsx("fog", { attach: "fog", args: [fog?.color ?? FOG_COLOR, fog?.near ?? 70, fog?.far ?? 260] })), lights ? (_jsxs(_Fragment, { children: [_jsx("hemisphereLight", { args: [ambient?.skyColor ?? HEMI_SKY, ambient?.groundColor ?? HEMI_GROUND, ambient?.intensity ?? 0.55] }), _jsx(ShadowCastingSun, { position: sunPosition, intensity: sun?.intensity ?? 0.85, color: sun?.color ?? SUN_COLOR })] })) : null] }));
|
|
122
|
+
return (_jsxs(_Fragment, { children: [sky === false ? null : (_jsx(SkyDome, { ...(sky ?? {}), sunDirection: sunPosition, sunColor: sun?.color ?? SUN_COLOR, sunIntensity: sun?.intensity ?? 1 })), clouds === undefined ? null : (_jsx(VolumetricClouds, { rules: resolveVolumetricClouds(clouds), sunDirection: sunPosition })), fog === false ? null : (_jsx("fog", { attach: "fog", args: [fog?.color ?? FOG_COLOR, fog?.near ?? 70, fog?.far ?? 260] })), lights ? (_jsxs(_Fragment, { children: [_jsx("hemisphereLight", { args: [ambient?.skyColor ?? HEMI_SKY, ambient?.groundColor ?? HEMI_GROUND, ambient?.intensity ?? 0.55] }), _jsx(ShadowCastingSun, { position: sunPosition, intensity: sun?.intensity ?? 0.85, color: sun?.color ?? SUN_COLOR })] })) : null] }));
|
|
123
|
+
}
|
|
124
|
+
/** True when at least one band overrides fog or sky — the trigger for the per-frame biome driver. */
|
|
125
|
+
function bandsDriveSkyOrFog(bands) {
|
|
126
|
+
return bands !== undefined && bands.some((band) => band.fog !== undefined || band.sky !== undefined);
|
|
127
|
+
}
|
|
128
|
+
/** Renders a fixed sky/sun/fog look sampled from `sky`'s preset (or, when `timeOfDay` is on but no clock drives it, its noon look). No per-frame updates.
|
|
129
|
+
* @internal
|
|
130
|
+
*/
|
|
131
|
+
export function SkyDaylight({ sky, lights = true, bands }) {
|
|
132
|
+
if (bandsDriveSkyOrFog(bands))
|
|
133
|
+
return _jsx(BiomeDaylight, { sky: sky, bands: bands, lights: lights });
|
|
134
|
+
return _jsx(StaticSkyDaylight, { sky: sky, lights: lights });
|
|
135
|
+
}
|
|
136
|
+
/** Dome-shape overrides (radius, haze, sun-glow) carried by the sky descriptor, spread onto `SkyDome`; unset fields keep the dome defaults. */
|
|
137
|
+
function skyDomeShape(sky) {
|
|
138
|
+
return {
|
|
139
|
+
...(sky.radius === undefined ? {} : { radius: sky.radius }),
|
|
140
|
+
...(sky.hazeStrength === undefined ? {} : { hazeStrength: sky.hazeStrength }),
|
|
141
|
+
...(sky.sunGlowStrength === undefined ? {} : { sunGlowStrength: sky.sunGlowStrength }),
|
|
142
|
+
};
|
|
106
143
|
}
|
|
107
|
-
|
|
108
|
-
export function SkyDaylight({ sky, lights = true }) {
|
|
144
|
+
function StaticSkyDaylight({ sky, lights }) {
|
|
109
145
|
const state = useMemo(() => daylightStateAt(SKY_PRESET_DAY_FRACTION[sky.preset], sky), [sky]);
|
|
110
|
-
return (_jsx(Daylight, { sky: { topColor: state.skyTop, horizonColor: state.skyBottom }, fog: { color: sky.fog?.color ?? state.background, near: sky.fog?.near, far: sky.fog?.far }, sun: { position: state.sunPosition, intensity: state.sunIntensity }, ambient: { intensity: state.ambientIntensity }, lights: lights }));
|
|
146
|
+
return (_jsx(Daylight, { sky: { topColor: state.skyTop, horizonColor: state.skyBottom, ...skyDomeShape(sky) }, fog: { color: sky.fog?.color ?? state.background, near: sky.fog?.near, far: sky.fog?.far }, sun: { position: state.sunPosition, intensity: state.sunIntensity }, ambient: { intensity: state.ambientIntensity }, lights: lights, clouds: sky.volumetricClouds }));
|
|
111
147
|
}
|
|
112
148
|
/**
|
|
113
149
|
* Drives sky/fog (and optional default lights) from the world clock when `sky.timeOfDay` and `clock`
|
|
114
150
|
* are both present. Authored `PlayableGame.lighting` is never rewritten — pass `lights={false}` so
|
|
115
151
|
* only dome colors and fog track the day fraction.
|
|
116
|
-
|
|
117
|
-
|
|
152
|
+
* @internal
|
|
153
|
+
*/
|
|
154
|
+
export function TimeOfDayDaylight({ sky, clock, lights = true, bands }) {
|
|
155
|
+
if (bandsDriveSkyOrFog(bands))
|
|
156
|
+
return _jsx(BiomeDaylight, { sky: sky, bands: bands, clock: clock, lights: lights });
|
|
118
157
|
if (!sky.timeOfDay || clock === undefined)
|
|
119
158
|
return _jsx(SkyDaylight, { sky: sky, lights: lights });
|
|
120
159
|
return _jsx(DrivenDaylight, { sky: sky, clock: clock, lights: lights });
|
|
121
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Per-frame biome fog/sky driver: cross-fades fog color/range, dome colors, and sun/ambient intensity
|
|
163
|
+
* along the camera's z through the terrain's `biomeBands`. Base look is the sky preset (or, when
|
|
164
|
+
* `sky.timeOfDay` and a `clock` are present, the live day fraction); bands ride on top.
|
|
165
|
+
*/
|
|
166
|
+
function BiomeDaylight({ sky, bands, clock, lights, }) {
|
|
167
|
+
const timeOfDay = sky.timeOfDay && clock !== undefined;
|
|
168
|
+
const baseFraction = timeOfDay ? clock.calendar().dayFraction : SKY_PRESET_DAY_FRACTION[sky.preset];
|
|
169
|
+
const initial = useMemo(() => daylightStateAt(baseFraction, sky), [sky, baseFraction]);
|
|
170
|
+
const sunRef = useRef(null);
|
|
171
|
+
const hemiRef = useRef(null);
|
|
172
|
+
const fogRef = useRef(null);
|
|
173
|
+
const skyMaterialRef = useRef(null);
|
|
174
|
+
const fogFallback = useMemo(() => ({
|
|
175
|
+
color: sky.fog?.color ?? initial.background,
|
|
176
|
+
near: sky.fog?.near ?? 70,
|
|
177
|
+
far: sky.fog?.far ?? 260,
|
|
178
|
+
density: 0,
|
|
179
|
+
}), [sky.fog?.color, sky.fog?.near, sky.fog?.far, initial.background]);
|
|
180
|
+
const skyFallback = useMemo(() => ({
|
|
181
|
+
horizonColor: initial.skyBottom,
|
|
182
|
+
zenithColor: initial.skyTop,
|
|
183
|
+
sunIntensity: initial.sunIntensity,
|
|
184
|
+
ambientIntensity: initial.ambientIntensity,
|
|
185
|
+
}), [initial.skyBottom, initial.skyTop, initial.sunIntensity, initial.ambientIntensity]);
|
|
186
|
+
const fogSampler = useMemo(() => createBiomeFogSampler(bands, fogFallback), [bands, fogFallback]);
|
|
187
|
+
const skySampler = useMemo(() => createBiomeSkySampler(bands, skyFallback), [bands, skyFallback]);
|
|
188
|
+
useFrame((state) => {
|
|
189
|
+
const base = timeOfDay ? daylightStateAt(clock.calendar().dayFraction, sky) : initial;
|
|
190
|
+
const z = state.camera.position.z;
|
|
191
|
+
const fog = fogSampler(z);
|
|
192
|
+
const skyValue = skySampler(z);
|
|
193
|
+
const fogNode = fogRef.current;
|
|
194
|
+
if (fogNode !== null) {
|
|
195
|
+
fogNode.color.set(fog.color);
|
|
196
|
+
fogNode.near = fog.near;
|
|
197
|
+
fogNode.far = fog.far;
|
|
198
|
+
}
|
|
199
|
+
const skyMaterial = skyMaterialRef.current;
|
|
200
|
+
if (skyMaterial !== null) {
|
|
201
|
+
skyMaterial.uniforms.topColor.value.set(skyValue.zenithColor);
|
|
202
|
+
skyMaterial.uniforms.bottomColor.value.set(skyValue.horizonColor);
|
|
203
|
+
}
|
|
204
|
+
const sun = sunRef.current;
|
|
205
|
+
if (sun !== null) {
|
|
206
|
+
sun.position.set(base.sunPosition[0], base.sunPosition[1], base.sunPosition[2]);
|
|
207
|
+
sun.intensity = skyValue.sunIntensity;
|
|
208
|
+
}
|
|
209
|
+
const hemi = hemiRef.current;
|
|
210
|
+
if (hemi !== null)
|
|
211
|
+
hemi.intensity = skyValue.ambientIntensity;
|
|
212
|
+
});
|
|
213
|
+
return (_jsxs(_Fragment, { children: [_jsx(SkyDome, { topColor: initial.skyTop, horizonColor: initial.skyBottom, materialRef: skyMaterialRef, ...skyDomeShape(sky) }), sky.volumetricClouds === undefined ? null : (_jsx(VolumetricClouds, { rules: resolveVolumetricClouds(sky.volumetricClouds), sunDirection: initial.sunPosition })), _jsx("fog", { attach: "fog", ref: fogRef, args: [fogFallback.color, fogFallback.near, fogFallback.far] }), lights ? (_jsxs(_Fragment, { children: [_jsx("hemisphereLight", { ref: hemiRef, args: [HEMI_SKY, HEMI_GROUND, initial.ambientIntensity] }), _jsx("directionalLight", { ref: sunRef, position: initial.sunPosition, intensity: initial.sunIntensity, color: SUN_COLOR, castShadow: true })] })) : null] }));
|
|
214
|
+
}
|
|
122
215
|
function DrivenDaylight({ sky, clock, lights, }) {
|
|
123
216
|
const initial = useMemo(() => daylightStateAt(clock.calendar().dayFraction, sky), [clock, sky]);
|
|
124
217
|
const sunRef = useRef(null);
|
|
@@ -144,5 +237,5 @@ function DrivenDaylight({ sky, clock, lights, }) {
|
|
|
144
237
|
skyMaterial.uniforms.bottomColor.value.set(state.skyBottom);
|
|
145
238
|
}
|
|
146
239
|
});
|
|
147
|
-
return (_jsxs(_Fragment, { children: [_jsx(SkyDome, { topColor: initial.skyTop, horizonColor: initial.skyBottom, materialRef: skyMaterialRef }), _jsx("fog", { attach: "fog", ref: fogRef, args: [sky.fog?.color ?? initial.background, sky.fog?.near ?? 70, sky.fog?.far ?? 260] }), lights ? (_jsxs(_Fragment, { children: [_jsx("hemisphereLight", { ref: hemiRef, args: [HEMI_SKY, HEMI_GROUND, initial.ambientIntensity] }), _jsx("directionalLight", { ref: sunRef, position: initial.sunPosition, intensity: initial.sunIntensity, color: SUN_COLOR, castShadow: true })] })) : null] }));
|
|
240
|
+
return (_jsxs(_Fragment, { children: [_jsx(SkyDome, { topColor: initial.skyTop, horizonColor: initial.skyBottom, materialRef: skyMaterialRef, ...skyDomeShape(sky) }), sky.volumetricClouds === undefined ? null : (_jsx(VolumetricClouds, { rules: resolveVolumetricClouds(sky.volumetricClouds), sunDirection: initial.sunPosition })), _jsx("fog", { attach: "fog", ref: fogRef, args: [sky.fog?.color ?? initial.background, sky.fog?.near ?? 70, sky.fog?.far ?? 260] }), lights ? (_jsxs(_Fragment, { children: [_jsx("hemisphereLight", { ref: hemiRef, args: [HEMI_SKY, HEMI_GROUND, initial.ambientIntensity] }), _jsx("directionalLight", { ref: sunRef, position: initial.sunPosition, intensity: initial.sunIntensity, color: SUN_COLOR, castShadow: true })] })) : null] }));
|
|
148
241
|
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useRef } from "react";
|
|
3
3
|
import { useFrame } from "@react-three/fiber";
|
|
4
|
+
import { useTexture } from "@react-three/drei";
|
|
5
|
+
import * as THREE from "three";
|
|
4
6
|
import { useGameContext } from "@jgengine/react/provider";
|
|
5
7
|
import { resolveBuildingPalette } from "@jgengine/core/world/buildings";
|
|
6
8
|
import { resolveStructureBuildings } from "@jgengine/core/world/environmentSummary";
|
|
7
9
|
import { createTerrainPaletteSampler, resolveEnvironmentField, resolveTerrainDetail, resolveTerrainPalette, } from "@jgengine/core/world/terrain";
|
|
8
10
|
import { GroundPad } from "./GroundPad.js";
|
|
9
11
|
import { RoadRibbons } from "./RoadRibbons.js";
|
|
12
|
+
import { terrainGroundColorSampler } from "./terrainGroundColor.js";
|
|
10
13
|
import { InstancedBuildings } from "../structures/GeneratedBuilding.js";
|
|
11
14
|
import { GrassField } from "../terrain/GrassField.js";
|
|
12
15
|
import { CarvedTerrain } from "../terrain/CarvedTerrain.js";
|
|
@@ -15,6 +18,26 @@ import { Ocean } from "../water/Ocean.js";
|
|
|
15
18
|
import { RainField } from "../weather/RainField.js";
|
|
16
19
|
import { SnowField } from "../weather/SnowField.js";
|
|
17
20
|
import { WeatherUniformProvider } from "../weather/weatherUniforms.js";
|
|
21
|
+
function TexturedTerrainGround({ detail, layout, }) {
|
|
22
|
+
const maps = detail.material.maps;
|
|
23
|
+
const textures = useTexture({
|
|
24
|
+
color: maps.color,
|
|
25
|
+
normal: maps.normal,
|
|
26
|
+
roughness: maps.roughness,
|
|
27
|
+
ao: maps.ao,
|
|
28
|
+
});
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
textures.color.colorSpace = THREE.SRGBColorSpace;
|
|
31
|
+
for (const texture of Object.values(textures)) {
|
|
32
|
+
texture.wrapS = THREE.RepeatWrapping;
|
|
33
|
+
texture.wrapT = THREE.RepeatWrapping;
|
|
34
|
+
texture.needsUpdate = true;
|
|
35
|
+
}
|
|
36
|
+
}, [textures]);
|
|
37
|
+
const material = useMemo(() => createTerrainDetailMaterial(detail, textures).material, [detail, textures]);
|
|
38
|
+
useEffect(() => () => material.dispose(), [material]);
|
|
39
|
+
return (_jsx(CarvedTerrain, { field: layout.field, size: layout.size, segments: layout.segments, colors: layout.colors, heightRange: layout.heightRange, paletteAt: layout.paletteAt, center: layout.center, roughness: 0.94, surfaceMaterial: material }));
|
|
40
|
+
}
|
|
18
41
|
function TerrainGround({ terrain, field, center, }) {
|
|
19
42
|
const palette = useMemo(() => resolveTerrainPalette(terrain), [terrain]);
|
|
20
43
|
const paletteAt = useMemo(() => {
|
|
@@ -38,16 +61,20 @@ function TerrainGround({ terrain, field, center, }) {
|
|
|
38
61
|
const swing = terrain.height * 1.2;
|
|
39
62
|
return [base - swing, base + swing];
|
|
40
63
|
}, [terrain.baseHeight, terrain.height]);
|
|
41
|
-
const
|
|
64
|
+
const resolvedDetail = useMemo(() => (terrain.detail === undefined ? undefined : resolveTerrainDetail(terrain.detail, terrain.waterLevel)), [terrain.detail, terrain.waterLevel]);
|
|
65
|
+
const proceduralMaterial = useMemo(() => resolvedDetail === undefined || resolvedDetail.material !== undefined
|
|
42
66
|
? undefined
|
|
43
|
-
: createTerrainDetailMaterial(
|
|
44
|
-
useEffect(() => () =>
|
|
45
|
-
|
|
67
|
+
: createTerrainDetailMaterial(resolvedDetail).material, [resolvedDetail]);
|
|
68
|
+
useEffect(() => () => proceduralMaterial?.dispose(), [proceduralMaterial]);
|
|
69
|
+
if (resolvedDetail?.material !== undefined) {
|
|
70
|
+
return (_jsx(TexturedTerrainGround, { detail: resolvedDetail, layout: { field, size, segments: terrain.segments, colors, heightRange, paletteAt, center } }));
|
|
71
|
+
}
|
|
72
|
+
return (_jsx(CarvedTerrain, { field: field, size: size, segments: terrain.segments, colors: colors, heightRange: heightRange, paletteAt: paletteAt, center: center, roughness: 0.94, surfaceMaterial: proceduralMaterial }));
|
|
46
73
|
}
|
|
47
74
|
function areaCenter(area) {
|
|
48
75
|
return area.position ?? [0, 0];
|
|
49
76
|
}
|
|
50
|
-
function Vegetation({ grass, field }) {
|
|
77
|
+
function Vegetation({ grass, field, groundColor, }) {
|
|
51
78
|
const [cx, cz] = areaCenter(grass.area);
|
|
52
79
|
const heightAt = useMemo(() => cx === 0 && cz === 0
|
|
53
80
|
? field.sampleHeight
|
|
@@ -56,7 +83,15 @@ function Vegetation({ grass, field }) {
|
|
|
56
83
|
const wind = useMemo(() => ({ strength: grass.windStrength }), [grass.windStrength]);
|
|
57
84
|
const colorBase = grass.colors[0];
|
|
58
85
|
const colorTip = grass.colors[grass.colors.length - 1];
|
|
59
|
-
return (_jsx("group", { position: [cx, 0, cz], children: _jsx(GrassField, { area: area, density: grass.density, seed: grass.seed, bladeHeight: grass.bladeHeight, bladeWidth: grass.bladeWidth, heightAt: heightAt, colorBase: colorBase, colorTip: colorTip, wind: wind, frustumCulled: true }) }));
|
|
86
|
+
return (_jsx("group", { position: [cx, 0, cz], children: _jsx(GrassField, { area: area, count: Math.max(1500, Math.min(250000, Math.ceil(grass.density * grass.area.w * grass.area.d))), density: grass.density, seed: grass.seed, bladeHeight: grass.bladeHeight, bladeWidth: grass.bladeWidth, heightAt: heightAt, colorBase: colorBase, colorTip: colorTip, colorGround: groundColor, wind: wind, edgeFeather: Math.min(8, Math.max(1.5, Math.min(grass.area.w, grass.area.d) * 0.12)), frustumCulled: true }) }));
|
|
87
|
+
}
|
|
88
|
+
/** Terrain color under a grass patch center — so blade roots blend into the ground they stand on. */
|
|
89
|
+
function vegetationGroundColor(terrain, field, grass) {
|
|
90
|
+
const sampler = terrainGroundColorSampler(terrain, field);
|
|
91
|
+
if (sampler === undefined)
|
|
92
|
+
return undefined;
|
|
93
|
+
const [cx, cz] = areaCenter(grass.area);
|
|
94
|
+
return sampler(cx, cz);
|
|
60
95
|
}
|
|
61
96
|
function weatherVolume(area) {
|
|
62
97
|
return [area.w, area.h ?? 60, area.d];
|
|
@@ -64,9 +99,9 @@ function weatherVolume(area) {
|
|
|
64
99
|
function Precipitation({ weather }) {
|
|
65
100
|
const [cx, cz] = areaCenter(weather.area);
|
|
66
101
|
if (weather.kind === "rain") {
|
|
67
|
-
return (_jsx("group", { position: [cx, 0, cz], children: _jsx(RainField, { density: weather.density, speed: weather.speed, length: weather.dropLength, color: weather.color, wind: [weather.wind[0], 0, weather.wind[1]], volume: weatherVolume(weather.area) }) }));
|
|
102
|
+
return (_jsx("group", { position: [cx, 0, cz], children: _jsx(RainField, { density: weather.density, speed: weather.speed, length: weather.dropLength, width: weather.width, opacity: weather.opacity, color: weather.color, wind: [weather.wind[0], 0, weather.wind[1]], volume: weatherVolume(weather.area) }) }));
|
|
68
103
|
}
|
|
69
|
-
return (_jsx("group", { position: [cx, 0, cz], children: _jsx(SnowField, { density: weather.density, speed: weather.speed, size: weather.flakeSize, sway: weather.drift, color: weather.color, wind: [weather.wind[0], 0, weather.wind[1]], volume: weatherVolume(weather.area) }) }));
|
|
104
|
+
return (_jsx("group", { position: [cx, 0, cz], children: _jsx(SnowField, { density: weather.density, speed: weather.speed, size: weather.flakeSize, sway: weather.drift, opacity: weather.opacity, color: weather.color, wind: [weather.wind[0], 0, weather.wind[1]], volume: weatherVolume(weather.area) }) }));
|
|
70
105
|
}
|
|
71
106
|
function Weather({ weather }) {
|
|
72
107
|
return (_jsx(WeatherUniformProvider, { children: weather.map((entry, index) => (_jsx(Precipitation, { weather: entry, index: index }, `${entry.kind}-${index}`))) }));
|
|
@@ -119,5 +154,5 @@ export function EnvironmentScene({ feature }) {
|
|
|
119
154
|
const pads = feature.pads ?? [];
|
|
120
155
|
const islands = feature.islands ?? [];
|
|
121
156
|
const roads = feature.roads ?? [];
|
|
122
|
-
return (_jsxs(_Fragment, { children: [feature.terrain !== undefined ? _jsx(TerrainGround, { terrain: feature.terrain, field: field }) : null, islands.map((entry, index) => (_jsx(TerrainGround, { terrain: entry, field: field, center: entry.origin }, `island-${index}`))), water.map((ocean, index) => (_jsx(Water, { ocean: ocean }, `ocean-${index}`))), roads.map((entry, index) => (_jsx(RoadRibbons, { road: entry, field: field }, `road-${index}`))), structures.map((entry, index) => (_jsx(Structures, { structures: entry, field: field }, `structures-${index}`))), pads.map((entry, index) => (_jsx(GroundPad, { pad: entry, field: field }, `pad-${index}`))), vegetation.map((grass, index) => (_jsx(Vegetation, { grass: grass, field: field }, `grass-${index}`))), feature.weather !== undefined && feature.weather.length > 0 ? (_jsx(Weather, { weather: feature.weather })) : null] }));
|
|
157
|
+
return (_jsxs(_Fragment, { children: [feature.terrain !== undefined ? _jsx(TerrainGround, { terrain: feature.terrain, field: field }) : null, islands.map((entry, index) => (_jsx(TerrainGround, { terrain: entry, field: field, center: entry.origin }, `island-${index}`))), water.map((ocean, index) => (_jsx(Water, { ocean: ocean }, `ocean-${index}`))), roads.map((entry, index) => (_jsx(RoadRibbons, { road: entry, field: field }, `road-${index}`))), structures.map((entry, index) => (_jsx(Structures, { structures: entry, field: field }, `structures-${index}`))), pads.map((entry, index) => (_jsx(GroundPad, { pad: entry, field: field }, `pad-${index}`))), vegetation.map((grass, index) => (_jsx(Vegetation, { grass: grass, field: field, groundColor: vegetationGroundColor(feature.terrain, field, grass) }, `grass-${index}`))), feature.weather !== undefined && feature.weather.length > 0 ? (_jsx(Weather, { weather: feature.weather })) : null] }));
|
|
123
158
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { VolumetricCloudsRules } from "@jgengine/core/world/volumetricClouds";
|
|
2
|
+
/** Props for {@link VolumetricClouds} — fully-resolved cloud rules plus the shared sun direction. */
|
|
3
|
+
export interface VolumetricCloudsProps {
|
|
4
|
+
rules: VolumetricCloudsRules;
|
|
5
|
+
/** Direction toward the sun (same vector `SkyDome`/`Daylight` use for the sun disc). */
|
|
6
|
+
sunDirection: readonly [number, number, number];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A raymarched volumetric cloud slab: a large horizontal box straddling `[height, height+thickness]`,
|
|
10
|
+
* following the camera in XZ (like `SkyDome`), whose fragment shader raymarches 3D FBM noise inside
|
|
11
|
+
* the slab and accumulates density-weighted color with a sun-facing forward-scatter term. Mounted
|
|
12
|
+
* from `Daylight`/`SkyDaylight`/`TimeOfDayDaylight` whenever a sky descriptor carries
|
|
13
|
+
* `volumetricClouds`; off by default (the component itself is simply not rendered). A game never
|
|
14
|
+
* mounts this directly — author clouds via `sky({ volumetricClouds })`.
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export declare function VolumetricClouds({ rules, sunDirection }: VolumetricCloudsProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame } from "@react-three/fiber";
|
|
3
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
/**
|
|
6
|
+
* Camera-centered proxy dome radius. Must sit inside the default camera far plane
|
|
7
|
+
* (`CAMERA_FRUSTUM_DEFAULTS.far` = 300) — a bigger sphere is clipped whole and renders nothing. The
|
|
8
|
+
* raymarch works in world space against the absolute cloud slab, so this size only has to cover the
|
|
9
|
+
* screen (camera is always at the dome center), never bound the clouds.
|
|
10
|
+
*/
|
|
11
|
+
const INSIDE_FRUSTUM_DOME_RADIUS = 240;
|
|
12
|
+
function hashSeed(text) {
|
|
13
|
+
let h = 2166136261 >>> 0;
|
|
14
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
15
|
+
h ^= text.charCodeAt(i);
|
|
16
|
+
h = Math.imul(h, 16777619);
|
|
17
|
+
}
|
|
18
|
+
return (h >>> 0) % 100000;
|
|
19
|
+
}
|
|
20
|
+
function normalizeVec3(v) {
|
|
21
|
+
const out = new THREE.Vector3(v[0], v[1], v[2]);
|
|
22
|
+
return out.lengthSq() === 0 ? new THREE.Vector3(0, 1, 0) : out.normalize();
|
|
23
|
+
}
|
|
24
|
+
const CLOUD_VERTEX_SHADER = /* glsl */ `
|
|
25
|
+
varying vec3 vJgCloudWorldPos;
|
|
26
|
+
void main() {
|
|
27
|
+
vJgCloudWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
|
|
28
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
29
|
+
}
|
|
30
|
+
`;
|
|
31
|
+
const CLOUD_FRAGMENT_SHADER = /* glsl */ `
|
|
32
|
+
uniform vec3 uCameraPos;
|
|
33
|
+
uniform vec3 uColor;
|
|
34
|
+
uniform vec3 uSunColor;
|
|
35
|
+
uniform vec3 uSunDirection;
|
|
36
|
+
uniform float uSunScatter;
|
|
37
|
+
uniform float uCoverage;
|
|
38
|
+
uniform float uDensity;
|
|
39
|
+
uniform float uBaseHeight;
|
|
40
|
+
uniform float uThickness;
|
|
41
|
+
uniform float uScale;
|
|
42
|
+
uniform float uSpeed;
|
|
43
|
+
uniform float uTime;
|
|
44
|
+
uniform float uSeedOffset;
|
|
45
|
+
varying vec3 vJgCloudWorldPos;
|
|
46
|
+
|
|
47
|
+
float jgCloudHash13(vec3 p){
|
|
48
|
+
p = fract(p * 0.1031 + uSeedOffset * 0.0007);
|
|
49
|
+
p += dot(p, p.yzx + 33.33);
|
|
50
|
+
return fract((p.x + p.y) * p.z);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
float jgCloudNoise(vec3 p){
|
|
54
|
+
vec3 i = floor(p);
|
|
55
|
+
vec3 f = fract(p);
|
|
56
|
+
f = f * f * (3.0 - 2.0 * f);
|
|
57
|
+
float n000 = jgCloudHash13(i + vec3(0.0, 0.0, 0.0));
|
|
58
|
+
float n100 = jgCloudHash13(i + vec3(1.0, 0.0, 0.0));
|
|
59
|
+
float n010 = jgCloudHash13(i + vec3(0.0, 1.0, 0.0));
|
|
60
|
+
float n110 = jgCloudHash13(i + vec3(1.0, 1.0, 0.0));
|
|
61
|
+
float n001 = jgCloudHash13(i + vec3(0.0, 0.0, 1.0));
|
|
62
|
+
float n101 = jgCloudHash13(i + vec3(1.0, 0.0, 1.0));
|
|
63
|
+
float n011 = jgCloudHash13(i + vec3(0.0, 1.0, 1.0));
|
|
64
|
+
float n111 = jgCloudHash13(i + vec3(1.0, 1.0, 1.0));
|
|
65
|
+
return mix(
|
|
66
|
+
mix(mix(n000, n100, f.x), mix(n010, n110, f.x), f.y),
|
|
67
|
+
mix(mix(n001, n101, f.x), mix(n011, n111, f.x), f.y),
|
|
68
|
+
f.z
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
float jgCloudFbm(vec3 p){
|
|
73
|
+
float v = 0.0;
|
|
74
|
+
float a = 0.5;
|
|
75
|
+
for (int i = 0; i < 5; i++) {
|
|
76
|
+
v += a * jgCloudNoise(p);
|
|
77
|
+
p = p * 2.03 + vec3(11.3, 7.1, 5.7);
|
|
78
|
+
a *= 0.5;
|
|
79
|
+
}
|
|
80
|
+
return v;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Density of the cloud slab at a world point: a 3D-FBM puff field carved by a coverage threshold and
|
|
84
|
+
// shaped by a cumulus vertical profile (rounded base, eroded anvil top). 0 outside the slab.
|
|
85
|
+
float jgCloudDensity(vec3 pos){
|
|
86
|
+
float heightFrac = (pos.y - uBaseHeight) / uThickness;
|
|
87
|
+
if (heightFrac < 0.0 || heightFrac > 1.0) return 0.0;
|
|
88
|
+
float profile = smoothstep(0.0, 0.2, heightFrac) * smoothstep(1.0, 0.5, heightFrac);
|
|
89
|
+
vec3 wind = vec3(uTime * uSpeed, 0.0, uTime * uSpeed * 0.6);
|
|
90
|
+
vec3 sp = (pos + wind) / uScale;
|
|
91
|
+
float shape = jgCloudFbm(sp);
|
|
92
|
+
float threshold = mix(0.62, 0.2, uCoverage);
|
|
93
|
+
float d = smoothstep(threshold, threshold + 0.3, shape * profile + profile * 0.12);
|
|
94
|
+
return d * profile * uDensity;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// March a few steps toward the sun and turn the accumulated density into transmittance (Beer's law) —
|
|
98
|
+
// the self-shadowing that gives the clouds volume instead of a flat wash.
|
|
99
|
+
float jgCloudLight(vec3 pos){
|
|
100
|
+
vec3 ldir = normalize(uSunDirection);
|
|
101
|
+
float stepL = uThickness * 0.2;
|
|
102
|
+
float dens = 0.0;
|
|
103
|
+
vec3 p = pos;
|
|
104
|
+
for (int i = 0; i < 4; i++) {
|
|
105
|
+
p += ldir * stepL;
|
|
106
|
+
dens += jgCloudDensity(p);
|
|
107
|
+
}
|
|
108
|
+
return exp(-dens * stepL * 1.6);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
void main() {
|
|
112
|
+
vec3 rayOrigin = uCameraPos;
|
|
113
|
+
vec3 rayDir = normalize(vJgCloudWorldPos - uCameraPos);
|
|
114
|
+
if (rayDir.y < 0.015) discard;
|
|
115
|
+
|
|
116
|
+
float tBottom = (uBaseHeight - rayOrigin.y) / rayDir.y;
|
|
117
|
+
float tTop = (uBaseHeight + uThickness - rayOrigin.y) / rayDir.y;
|
|
118
|
+
float tNear = max(min(tBottom, tTop), 0.0);
|
|
119
|
+
float tFar = max(tBottom, tTop);
|
|
120
|
+
if (tFar <= tNear) discard;
|
|
121
|
+
tFar = min(tFar, tNear + uThickness * 6.0);
|
|
122
|
+
|
|
123
|
+
const int STEPS = 32;
|
|
124
|
+
float stepLen = (tFar - tNear) / float(STEPS);
|
|
125
|
+
float t = tNear + stepLen * 0.5;
|
|
126
|
+
float transmittance = 1.0;
|
|
127
|
+
vec3 color = vec3(0.0);
|
|
128
|
+
float sunDot = max(dot(rayDir, normalize(uSunDirection)), 0.0);
|
|
129
|
+
float silver = pow(sunDot, 5.0) * uSunScatter;
|
|
130
|
+
|
|
131
|
+
for (int i = 0; i < STEPS; i++) {
|
|
132
|
+
vec3 pos = rayOrigin + rayDir * t;
|
|
133
|
+
float dens = jgCloudDensity(pos);
|
|
134
|
+
if (dens > 0.002) {
|
|
135
|
+
float light = jgCloudLight(pos);
|
|
136
|
+
float powder = 1.0 - exp(-dens * 3.0);
|
|
137
|
+
vec3 lit = mix(uColor, uSunColor, light) * (0.55 + 0.45 * powder) + uSunColor * silver * 0.5;
|
|
138
|
+
float dt = dens * stepLen * 1.3;
|
|
139
|
+
float a = 1.0 - exp(-dt);
|
|
140
|
+
color += transmittance * a * lit;
|
|
141
|
+
transmittance *= 1.0 - a;
|
|
142
|
+
if (transmittance < 0.02) break;
|
|
143
|
+
}
|
|
144
|
+
t += stepLen;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
float alpha = (1.0 - transmittance) * smoothstep(0.015, 0.22, rayDir.y);
|
|
148
|
+
if (alpha < 0.01) discard;
|
|
149
|
+
gl_FragColor = vec4(color, alpha);
|
|
150
|
+
}
|
|
151
|
+
`;
|
|
152
|
+
/**
|
|
153
|
+
* A raymarched volumetric cloud slab: a large horizontal box straddling `[height, height+thickness]`,
|
|
154
|
+
* following the camera in XZ (like `SkyDome`), whose fragment shader raymarches 3D FBM noise inside
|
|
155
|
+
* the slab and accumulates density-weighted color with a sun-facing forward-scatter term. Mounted
|
|
156
|
+
* from `Daylight`/`SkyDaylight`/`TimeOfDayDaylight` whenever a sky descriptor carries
|
|
157
|
+
* `volumetricClouds`; off by default (the component itself is simply not rendered). A game never
|
|
158
|
+
* mounts this directly — author clouds via `sky({ volumetricClouds })`.
|
|
159
|
+
* @internal
|
|
160
|
+
*/
|
|
161
|
+
export function VolumetricClouds({ rules, sunDirection }) {
|
|
162
|
+
const meshRef = useRef(null);
|
|
163
|
+
const timeRef = useRef(0);
|
|
164
|
+
const material = useMemo(() => {
|
|
165
|
+
return new THREE.ShaderMaterial({
|
|
166
|
+
uniforms: {
|
|
167
|
+
uCameraPos: { value: new THREE.Vector3() },
|
|
168
|
+
uColor: { value: new THREE.Color(rules.color) },
|
|
169
|
+
uSunColor: { value: new THREE.Color(rules.sunColor) },
|
|
170
|
+
uSunDirection: { value: normalizeVec3(sunDirection) },
|
|
171
|
+
uSunScatter: { value: rules.sunScatter },
|
|
172
|
+
uCoverage: { value: rules.coverage },
|
|
173
|
+
uDensity: { value: rules.density },
|
|
174
|
+
uBaseHeight: { value: rules.height },
|
|
175
|
+
uThickness: { value: rules.thickness },
|
|
176
|
+
uScale: { value: rules.scale },
|
|
177
|
+
uSpeed: { value: rules.speed },
|
|
178
|
+
uTime: { value: 0 },
|
|
179
|
+
uSeedOffset: { value: hashSeed(rules.seed) },
|
|
180
|
+
},
|
|
181
|
+
vertexShader: CLOUD_VERTEX_SHADER,
|
|
182
|
+
fragmentShader: CLOUD_FRAGMENT_SHADER,
|
|
183
|
+
transparent: true,
|
|
184
|
+
depthWrite: false,
|
|
185
|
+
side: THREE.BackSide,
|
|
186
|
+
fog: false,
|
|
187
|
+
});
|
|
188
|
+
}, [rules, sunDirection]);
|
|
189
|
+
useEffect(() => () => material.dispose(), [material]);
|
|
190
|
+
useFrame((state, delta) => {
|
|
191
|
+
timeRef.current += delta;
|
|
192
|
+
const mesh = meshRef.current;
|
|
193
|
+
if (mesh !== null) {
|
|
194
|
+
mesh.position.x = state.camera.position.x;
|
|
195
|
+
mesh.position.z = state.camera.position.z;
|
|
196
|
+
}
|
|
197
|
+
const uniforms = material.uniforms;
|
|
198
|
+
uniforms.uCameraPos.value.copy(state.camera.position);
|
|
199
|
+
uniforms.uTime.value = timeRef.current;
|
|
200
|
+
});
|
|
201
|
+
return (_jsx("mesh", { ref: meshRef, material: material, renderOrder: -0.5, frustumCulled: false, children: _jsx("sphereGeometry", { args: [INSIDE_FRUSTUM_DOME_RADIUS, 32, 16] }) }));
|
|
202
|
+
}
|
|
@@ -17,10 +17,12 @@ export declare const DEFAULT_DAY_AMBIENT_INTENSITY = 0.6;
|
|
|
17
17
|
export declare const DEFAULT_DAY_SKY_TOP = "#3fa4f2";
|
|
18
18
|
export declare const DEFAULT_DAY_SKY_BOTTOM = "#e3f4ff";
|
|
19
19
|
export declare const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", number>;
|
|
20
|
+
/** @internal */
|
|
20
21
|
export declare function lerpHexColor(a: string, b: string, t: number): string;
|
|
21
22
|
/**
|
|
22
23
|
* Samples the daylight cycle at a point in the day (0 = midnight, 0.25 = dawn, 0.5 = noon, 0.75 = dusk),
|
|
23
24
|
* lerping sun position/intensity, ambient intensity, and sky colors through a dawn/day/dusk/night
|
|
24
25
|
* keyframe table. `config` overrides the noon (peak-day) colors and intensities only.
|
|
25
|
-
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
26
28
|
export declare function daylightStateAt(dayFraction: number, config?: DaylightCycleConfig): DaylightState;
|
|
@@ -38,6 +38,7 @@ function clampByte(value) {
|
|
|
38
38
|
function rgbToHex(rgb) {
|
|
39
39
|
return `#${rgb.map((channel) => clampByte(channel).toString(16).padStart(2, "0")).join("")}`;
|
|
40
40
|
}
|
|
41
|
+
/** @internal */
|
|
41
42
|
export function lerpHexColor(a, b, t) {
|
|
42
43
|
const [ar, ag, ab] = hexToRgb(a);
|
|
43
44
|
const [br, bg, bb] = hexToRgb(b);
|
|
@@ -95,7 +96,8 @@ function segmentAt(wrapped, keyframes) {
|
|
|
95
96
|
* Samples the daylight cycle at a point in the day (0 = midnight, 0.25 = dawn, 0.5 = noon, 0.75 = dusk),
|
|
96
97
|
* lerping sun position/intensity, ambient intensity, and sky colors through a dawn/day/dusk/night
|
|
97
98
|
* keyframe table. `config` overrides the noon (peak-day) colors and intensities only.
|
|
98
|
-
|
|
99
|
+
* @internal
|
|
100
|
+
*/
|
|
99
101
|
export function daylightStateAt(dayFraction, config = {}) {
|
|
100
102
|
const wrapped = wrapFraction(dayFraction);
|
|
101
103
|
const keyframes = buildKeyframes(config);
|
|
@@ -8,6 +8,9 @@ export type PadShape = {
|
|
|
8
8
|
width: number;
|
|
9
9
|
depth: number;
|
|
10
10
|
};
|
|
11
|
+
/** @internal */
|
|
11
12
|
export declare function resolvePadShape(size: PadSize): PadShape;
|
|
13
|
+
/** @internal */
|
|
12
14
|
export declare function resolvePadSurfaceY(groundHeight: number, pad: Pick<PadEnvironmentDescriptor, "height" | "elevation">): number;
|
|
15
|
+
/** @internal */
|
|
13
16
|
export declare function resolvePadMeshY(groundHeight: number, pad: Pick<PadEnvironmentDescriptor, "height" | "elevation">): number;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export const PAD_THICKNESS = 0.1;
|
|
2
|
+
/** @internal */
|
|
2
3
|
export function resolvePadShape(size) {
|
|
3
4
|
return "radius" in size ? { circular: true, radius: size.radius } : { circular: false, width: size[0], depth: size[1] };
|
|
4
5
|
}
|
|
6
|
+
/** @internal */
|
|
5
7
|
export function resolvePadSurfaceY(groundHeight, pad) {
|
|
6
8
|
return pad.elevation ?? groundHeight + pad.height;
|
|
7
9
|
}
|
|
10
|
+
/** @internal */
|
|
8
11
|
export function resolvePadMeshY(groundHeight, pad) {
|
|
9
12
|
return resolvePadSurfaceY(groundHeight, pad) - PAD_THICKNESS / 2;
|
|
10
13
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { Daylight, SkyDaylight, SkyDome, TimeOfDayDaylight, type DaylightProps, type SkyDaylightProps, type SkyDomeProps, type TimeOfDayDaylightProps, } from "./Daylight.js";
|
|
2
2
|
export { EnvironmentScene, type EnvironmentSceneProps } from "./EnvironmentScene.js";
|
|
3
|
+
export { VolumetricClouds, type VolumetricCloudsProps } from "./VolumetricClouds.js";
|
|
3
4
|
export { daylightStateAt, lerpHexColor, SKY_PRESET_DAY_FRACTION, type DaylightCycleConfig, type DaylightState, } from "./daylightCycle.js";
|
|
4
5
|
export { resolveSkyLightOwnership, skyEmitsLights, type SkyLightOwnership, } from "./skyLightingPolicy.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { Daylight, SkyDaylight, SkyDome, TimeOfDayDaylight, } from "./Daylight.js";
|
|
2
2
|
export { EnvironmentScene } from "./EnvironmentScene.js";
|
|
3
|
+
export { VolumetricClouds } from "./VolumetricClouds.js";
|
|
3
4
|
export { daylightStateAt, lerpHexColor, SKY_PRESET_DAY_FRACTION, } from "./daylightCycle.js";
|
|
4
5
|
export { resolveSkyLightOwnership, skyEmitsLights, } from "./skyLightingPolicy.js";
|
|
@@ -6,5 +6,7 @@
|
|
|
6
6
|
* sky-owned lights when the game did not author lighting).
|
|
7
7
|
*/
|
|
8
8
|
export type SkyLightOwnership = "authored" | "sky-default";
|
|
9
|
+
/** @internal */
|
|
9
10
|
export declare function resolveSkyLightOwnership(hasAuthoredLighting: boolean): SkyLightOwnership;
|
|
11
|
+
/** @internal */
|
|
10
12
|
export declare function skyEmitsLights(ownership: SkyLightOwnership): boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TerrainEnvironmentDescriptor } from "@jgengine/core/world/features";
|
|
2
|
+
import { type TerrainField } from "@jgengine/core/world/terrain";
|
|
3
|
+
/**
|
|
4
|
+
* Terrain surface color at a world XZ — the palette low/high lerped by the sampled ground height,
|
|
5
|
+
* mirroring how `CarvedTerrain` shades its vertices. The seam that lets everything planted on the
|
|
6
|
+
* ground (grass roots, soil-patch borders) blend into the exact color it stands on.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export declare function terrainGroundColorSampler(terrain: Omit<TerrainEnvironmentDescriptor, "kind"> | undefined, field: TerrainField): ((x: number, z: number) => string) | undefined;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { resolveTerrainPalette } from "@jgengine/core/world/terrain";
|
|
3
|
+
/**
|
|
4
|
+
* Terrain surface color at a world XZ — the palette low/high lerped by the sampled ground height,
|
|
5
|
+
* mirroring how `CarvedTerrain` shades its vertices. The seam that lets everything planted on the
|
|
6
|
+
* ground (grass roots, soil-patch borders) blend into the exact color it stands on.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export function terrainGroundColorSampler(terrain, field) {
|
|
10
|
+
if (terrain === undefined)
|
|
11
|
+
return undefined;
|
|
12
|
+
const palette = resolveTerrainPalette(terrain);
|
|
13
|
+
const low = new THREE.Color(palette.low);
|
|
14
|
+
const high = new THREE.Color(palette.high);
|
|
15
|
+
const base = terrain.baseHeight ?? 0;
|
|
16
|
+
const swing = terrain.height * 1.2;
|
|
17
|
+
const scratch = new THREE.Color();
|
|
18
|
+
return (x, z) => {
|
|
19
|
+
const t = swing <= 0 ? 0.5 : THREE.MathUtils.clamp((field.sampleHeight(x, z) - (base - swing)) / (swing * 2), 0, 1);
|
|
20
|
+
return `#${scratch.copy(low).lerp(high, t).getHexString()}`;
|
|
21
|
+
};
|
|
22
|
+
}
|