@jgengine/shell 0.25.0 → 0.27.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 +109 -0
- package/dist/GamePlayerShell.d.ts +8 -0
- package/dist/GamePlayerShell.js +15 -1
- package/dist/Shell3dPresentation.js +29 -11
- package/dist/devtools/FrameTelemetry.d.ts +12 -0
- package/dist/devtools/FrameTelemetry.js +68 -0
- package/dist/devtools/agentBridge.js +2 -0
- package/dist/devtools/devtoolsReports.d.ts +2 -0
- package/dist/devtools/devtoolsReports.js +3 -0
- package/dist/devtools/frameSampler.d.ts +127 -0
- package/dist/devtools/frameSampler.js +345 -0
- package/dist/environment/AuthoredDaylight.d.ts +28 -0
- package/dist/environment/AuthoredDaylight.js +42 -0
- package/dist/environment/Daylight.d.ts +55 -7
- package/dist/environment/Daylight.js +126 -29
- package/dist/environment/EnvironmentScene.js +51 -8
- package/dist/environment/daylightCycle.d.ts +26 -4
- package/dist/environment/daylightCycle.js +36 -5
- package/dist/postfx/PostProcessing.d.ts +7 -1
- package/dist/postfx/PostProcessing.js +28 -2
- package/dist/postfx/autoExposure.d.ts +49 -0
- package/dist/postfx/autoExposure.js +189 -0
- package/dist/postfx/gradeShader.js +43 -4
- package/dist/presentationRecovery3d.js +2 -1
- package/dist/render/CascadedShadows.d.ts +3 -1
- package/dist/render/CascadedShadows.js +9 -1
- package/dist/render/EnvironmentLighting.d.ts +10 -1
- package/dist/render/EnvironmentLighting.js +11 -3
- package/dist/render/IrradianceVolumeLighting.d.ts +18 -0
- package/dist/render/IrradianceVolumeLighting.js +35 -0
- package/dist/render/SceneLighting.d.ts +14 -1
- package/dist/render/SceneLighting.js +29 -8
- package/dist/render/authoredGrade.d.ts +11 -0
- package/dist/render/authoredGrade.js +26 -0
- package/dist/render/decalGeometry.d.ts +26 -0
- package/dist/render/decalGeometry.js +54 -0
- package/dist/render/decalTexture.d.ts +13 -0
- package/dist/render/decalTexture.js +114 -0
- package/dist/render/modelWind.d.ts +28 -0
- package/dist/render/modelWind.js +103 -0
- package/dist/render/presentationLook.d.ts +15 -0
- package/dist/render/presentationLook.js +18 -0
- package/dist/render/stableTextureSet.d.ts +9 -0
- package/dist/render/stableTextureSet.js +24 -0
- package/dist/render/triplanarMaterial.d.ts +52 -0
- package/dist/render/triplanarMaterial.js +169 -0
- package/dist/render/useModelInstance.d.ts +1 -1
- package/dist/render/useModelInstance.js +3 -1
- package/dist/scatter/scatterModels.js +2 -0
- package/dist/scene/DecalRenderer.d.ts +2 -0
- package/dist/scene/DecalRenderer.js +74 -0
- package/dist/scene/GrassFieldRenderer.js +3 -2
- package/dist/scene/builtinSceneKindRenderers.js +2 -0
- package/dist/settings/appliedSettings.js +16 -13
- package/dist/terrain/GrassCover.d.ts +21 -0
- package/dist/terrain/GrassCover.js +84 -0
- package/dist/terrain/grassCoverGeometry.d.ts +40 -0
- package/dist/terrain/grassCoverGeometry.js +112 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +12 -0
- package/dist/terrain/terrainDetailMaterial.js +54 -1
- package/dist/terrain/terrainSurfaces.d.ts +6 -0
- package/dist/terrain/terrainSurfaces.js +10 -0
- package/dist/world/WorldScene.js +2 -1
- package/package.json +4 -4
- package/skills/jgengine/api.md +59 -6
- package/skills/jgengine/capabilities.md +4 -0
- package/skills/jgengine-ui/api.md +114 -23
- package/skills/jgengine-ui/capabilities.md +14 -0
- package/skills/jgengine-ui/reference-primitives.md +4 -2
- package/skills/jgengine-world/SKILL.md +7 -1
- package/skills/jgengine-world/api.md +222 -8
- package/skills/jgengine-world/capabilities.md +42 -2
- package/skills/jgengine-world/recipes/npc-senses.md +8 -2
- package/skills/jgengine-world/recipes/vehicle-feel.md +5 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
3
|
-
import { useEffect, useLayoutEffect, useMemo, useRef } from "react";
|
|
3
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import * as THREE from "three";
|
|
5
5
|
import { createBiomeFogSampler, createBiomeSkySampler } from "@jgengine/core/world/terrain";
|
|
6
6
|
import { resolveVolumetricClouds } from "@jgengine/core/world/volumetricClouds";
|
|
@@ -17,15 +17,23 @@ const SKY_TOP = "#3fa4f2";
|
|
|
17
17
|
const SKY_HORIZON = "#e3f4ff";
|
|
18
18
|
const FOG_COLOR = "#e9f6ff";
|
|
19
19
|
const SUN_COLOR = "#fff1c9";
|
|
20
|
+
const MOON_COLOR = "#cfdcf2";
|
|
20
21
|
const HEMI_SKY = "#bfe3ff";
|
|
21
22
|
const HEMI_GROUND = "#4c6b34";
|
|
23
|
+
/** Aim the dome's moon disc and dial its brightness; called per frame by the time-of-day drivers. @internal */
|
|
24
|
+
export function setSkyMoon(material, direction, color, intensity, starOpacity) {
|
|
25
|
+
material.uniforms.uMoonDirection.value.set(direction[0], direction[1], direction[2]).normalize();
|
|
26
|
+
material.uniforms.uMoonColor.value.set(color);
|
|
27
|
+
material.uniforms.uMoonIntensity.value = intensity;
|
|
28
|
+
material.uniforms.uStars.value = starOpacity;
|
|
29
|
+
}
|
|
22
30
|
/**
|
|
23
31
|
* Shadow-casting sun whose shadow box leads the camera each frame ({@link sunShadowFocus}), so
|
|
24
32
|
* grounded shadows stay crisp and soft-edged under the player anywhere in a large world. Time-of-day
|
|
25
33
|
* drivers aim it through `directionRef` and retint it through `lightRef`.
|
|
26
34
|
* @internal
|
|
27
35
|
*/
|
|
28
|
-
export function SunLight({ direction, directionRef, lightRef, intensity, color, }) {
|
|
36
|
+
export function SunLight({ direction, directionRef, lightRef, intensity, color, castShadow = true, }) {
|
|
29
37
|
const ownRef = useRef(null);
|
|
30
38
|
const ref = lightRef ?? ownRef;
|
|
31
39
|
const scratch = useMemo(() => ({ forward: new THREE.Vector3(), focus: { x: 0, z: 0 }, position: [0, 0, 0] }), []);
|
|
@@ -41,7 +49,15 @@ export function SunLight({ direction, directionRef, lightRef, intensity, color,
|
|
|
41
49
|
light.target.updateMatrixWorld();
|
|
42
50
|
});
|
|
43
51
|
const initial = sunLightPosition({ x: 0, z: 0 }, direction);
|
|
44
|
-
return (_jsx("directionalLight", { ref: ref, position: initial, intensity: intensity, color: color, castShadow:
|
|
52
|
+
return (_jsx("directionalLight", { ref: ref, position: initial, intensity: intensity, color: color, castShadow: castShadow, "shadow-mapSize-width": SUN_SHADOW.mapSize, "shadow-mapSize-height": SUN_SHADOW.mapSize, "shadow-camera-left": -SUN_SHADOW.halfExtent, "shadow-camera-right": SUN_SHADOW.halfExtent, "shadow-camera-top": SUN_SHADOW.halfExtent, "shadow-camera-bottom": -SUN_SHADOW.halfExtent, "shadow-camera-near": SUN_SHADOW.near, "shadow-camera-far": SUN_SHADOW.far, "shadow-bias": SUN_SHADOW.bias, "shadow-normalBias": SUN_SHADOW.normalBias, "shadow-radius": SUN_SHADOW.radius }));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The moon as a dim, shadowless key light opposite the sun, so a night scene keeps a lit and a
|
|
56
|
+
* shaded side instead of collapsing into flat ambient. Same camera-following aim as the sun.
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
export function MoonLight({ direction, directionRef, lightRef, intensity, color }) {
|
|
60
|
+
return _jsx(SunLight, { direction: direction, directionRef: directionRef, lightRef: lightRef, intensity: intensity, color: color, castShadow: false });
|
|
45
61
|
}
|
|
46
62
|
/**
|
|
47
63
|
* Sets the zenith and horizon colors a sky dome should show on screen. The dome converts them each
|
|
@@ -87,7 +103,7 @@ export function setSkyFogColor(fog, color) {
|
|
|
87
103
|
}
|
|
88
104
|
const fogDisplayColors = new WeakMap();
|
|
89
105
|
/** @internal */
|
|
90
|
-
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, cloudiness = 0.35, materialRef, } = {}) {
|
|
106
|
+
export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, sunDirection, sunColor = "#fff4d6", sunIntensity = 1, moonDirection, moonColor = MOON_COLOR, moonIntensity = 0, starOpacity = 0, hazeStrength = 0.5, sunGlowStrength = 1, cloudiness = 0.35, materialRef, } = {}) {
|
|
91
107
|
const meshRef = useRef(null);
|
|
92
108
|
const material = useMemo(() => {
|
|
93
109
|
const dome = new THREE.ShaderMaterial({
|
|
@@ -99,6 +115,10 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
99
115
|
uSunColor: { value: new THREE.Color(sunColor) },
|
|
100
116
|
uSunDirection: { value: sunDirection === undefined ? new THREE.Vector3(0, 1, 0) : normalizeVec3(sunDirection) },
|
|
101
117
|
uSunIntensity: { value: sunDirection === undefined ? 0 : sunIntensity },
|
|
118
|
+
uMoonColor: { value: new THREE.Color(moonColor) },
|
|
119
|
+
uMoonDirection: { value: moonDirection === undefined ? new THREE.Vector3(0, -1, 0) : normalizeVec3(moonDirection) },
|
|
120
|
+
uMoonIntensity: { value: moonDirection === undefined ? 0 : moonIntensity },
|
|
121
|
+
uStars: { value: starOpacity },
|
|
102
122
|
uHazeStrength: { value: hazeStrength },
|
|
103
123
|
uSunGlow: { value: sunGlowStrength },
|
|
104
124
|
uCloudiness: { value: cloudiness },
|
|
@@ -117,6 +137,10 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
117
137
|
uniform vec3 uSunColor;
|
|
118
138
|
uniform vec3 uSunDirection;
|
|
119
139
|
uniform float uSunIntensity;
|
|
140
|
+
uniform vec3 uMoonColor;
|
|
141
|
+
uniform vec3 uMoonDirection;
|
|
142
|
+
uniform float uMoonIntensity;
|
|
143
|
+
uniform float uStars;
|
|
120
144
|
uniform float uHazeStrength;
|
|
121
145
|
uniform float uSunGlow;
|
|
122
146
|
uniform float uCloudiness;
|
|
@@ -130,6 +154,16 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
130
154
|
mix(sHash(i + vec2(0.0, 1.0)), sHash(i + vec2(1.0, 1.0)), u.x), u.y);
|
|
131
155
|
}
|
|
132
156
|
float sFbm(vec2 p){ float v = 0.0; float a = 0.5; for (int i = 0; i < 5; i++){ v += a * sNoise(p); p *= 2.0; a *= 0.5; } return v; }
|
|
157
|
+
float h3(vec3 p){ return fract(sin(dot(p, vec3(12.9898, 78.233, 37.719))) * 43758.5453); }
|
|
158
|
+
float starField(vec3 dir){
|
|
159
|
+
vec3 q = dir * 170.0;
|
|
160
|
+
vec3 cell = floor(q);
|
|
161
|
+
float seed = h3(cell);
|
|
162
|
+
if (seed < 0.984) return 0.0;
|
|
163
|
+
vec3 jitter = fract(vec3(seed * 137.0, seed * 311.0, seed * 531.0)) - 0.5;
|
|
164
|
+
float d = length(fract(q) - 0.5 - jitter * 0.7);
|
|
165
|
+
return smoothstep(0.18, 0.0, d) * smoothstep(0.984, 0.999, seed);
|
|
166
|
+
}
|
|
133
167
|
void main() {
|
|
134
168
|
vec3 dir = normalize(vWorldPosition + vec3(0.0, offset, 0.0));
|
|
135
169
|
float h = max(dir.y, 0.0);
|
|
@@ -143,6 +177,16 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
143
177
|
float sd = max(dot(vd, normalize(uSunDirection)), 0.0);
|
|
144
178
|
float glow = pow(sd, 8.0) * 0.35 + pow(sd, 128.0) * 2.6;
|
|
145
179
|
col += uSunColor * glow * uSunIntensity * uSunGlow;
|
|
180
|
+
if (uStars > 0.001) {
|
|
181
|
+
float stars = starField(vd) * smoothstep(0.02, 0.22, vd.y) * smoothstep(0.35, 0.9, uStars);
|
|
182
|
+
col += vec3(0.86, 0.9, 1.0) * stars * uStars * 1.6;
|
|
183
|
+
}
|
|
184
|
+
if (uMoonIntensity > 0.001) {
|
|
185
|
+
float md = max(dot(vd, normalize(uMoonDirection)), 0.0);
|
|
186
|
+
float disc = smoothstep(0.99955, 0.99985, md);
|
|
187
|
+
float halo = pow(md, 512.0) * 0.5 + pow(md, 24.0) * 0.05;
|
|
188
|
+
col += uMoonColor * (disc * 2.2 + halo) * uMoonIntensity;
|
|
189
|
+
}
|
|
146
190
|
gl_FragColor = vec4(col, 1.0);
|
|
147
191
|
#include <tonemapping_fragment>
|
|
148
192
|
#include <colorspace_fragment>
|
|
@@ -154,7 +198,7 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
154
198
|
});
|
|
155
199
|
setSkyDomeColors(dome, topColor, horizonColor);
|
|
156
200
|
return dome;
|
|
157
|
-
}, [topColor, horizonColor, offset, exponent, sunColor, sunDirection, sunIntensity, hazeStrength, sunGlowStrength, cloudiness]);
|
|
201
|
+
}, [topColor, horizonColor, offset, exponent, sunColor, sunDirection, sunIntensity, moonDirection, moonColor, moonIntensity, starOpacity, hazeStrength, sunGlowStrength, cloudiness]);
|
|
158
202
|
useEffect(() => {
|
|
159
203
|
if (materialRef !== undefined)
|
|
160
204
|
materialRef.current = material;
|
|
@@ -189,21 +233,46 @@ function aimSkySun(material, position) {
|
|
|
189
233
|
material.uniforms.uSunDirection.value.set(position[0], position[1], position[2]).normalize();
|
|
190
234
|
}
|
|
191
235
|
/** @internal */
|
|
192
|
-
export function Daylight({ sky, fog, sun, ambient, lights = true, clouds } = {}) {
|
|
236
|
+
export function Daylight({ sky, fog, sun, ambient, moon, lights = true, clouds } = {}) {
|
|
193
237
|
const sunPosition = sun?.position ?? [120, 160, 70];
|
|
194
|
-
|
|
238
|
+
const moonIntensity = moon?.intensity ?? 0;
|
|
239
|
+
return (_jsxs(_Fragment, { children: [sky === false ? null : (_jsx(SkyDome, { ...(sky ?? {}), sunDirection: sunPosition, sunColor: sun?.color ?? SUN_COLOR, sunIntensity: sun?.intensity ?? 1, ...(moon?.position === undefined ? {} : { moonDirection: moon.position }), ...(moon?.color === undefined ? {} : { moonColor: moon.color }), moonIntensity: moonIntensity, starOpacity: moon?.stars ?? 0 })), clouds === undefined ? null : (_jsx(VolumetricClouds, { rules: resolveVolumetricClouds(clouds), sunDirection: sunPosition })), fog === false ? null : (_jsx(SkyFog, { color: fog?.color ?? FOG_COLOR, near: fog?.near ?? 70, far: fog?.far ?? 260 })), lights ? (_jsxs(_Fragment, { children: [_jsx("hemisphereLight", { args: [ambient?.skyColor ?? HEMI_SKY, ambient?.groundColor ?? HEMI_GROUND, ambient?.intensity ?? 0.55] }), _jsx(SunLight, { direction: sunPosition, intensity: sun?.intensity ?? 0.85, color: sun?.color ?? SUN_COLOR }), moonIntensity > 0 && moon?.position !== undefined ? (_jsx(MoonLight, { direction: moon.position, intensity: moonIntensity, color: moon.color ?? MOON_COLOR })) : null] })) : null] }));
|
|
195
240
|
}
|
|
196
241
|
/** True when at least one band overrides fog or sky — the trigger for the per-frame biome driver. */
|
|
197
242
|
function bandsDriveSkyOrFog(bands) {
|
|
198
243
|
return bands !== undefined && bands.some((band) => band.fog !== undefined || band.sky !== undefined);
|
|
199
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Mount state for the moon: it re-renders only when the moon crosses the horizon. A zero-intensity
|
|
247
|
+
* directional light still costs a per-fragment loop in every lit material, so a day sky must not
|
|
248
|
+
* carry it.
|
|
249
|
+
* @internal
|
|
250
|
+
*/
|
|
251
|
+
export function useMoonAboveHorizon(initialUp) {
|
|
252
|
+
const [up, setUp] = useState(initialUp);
|
|
253
|
+
const upRef = useRef(up);
|
|
254
|
+
return [
|
|
255
|
+
up,
|
|
256
|
+
(intensity) => {
|
|
257
|
+
const next = intensity > 0;
|
|
258
|
+
if (next === upRef.current)
|
|
259
|
+
return;
|
|
260
|
+
upRef.current = next;
|
|
261
|
+
setUp(next);
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
}
|
|
265
|
+
/** Fog range: the sky's own fields win, then the authored backdrop, then the caller's default. */
|
|
266
|
+
function skyFogRange(sky, fogRange, fallback) {
|
|
267
|
+
return { near: sky.fog?.near ?? fogRange?.near ?? fallback.near, far: sky.fog?.far ?? fogRange?.far ?? fallback.far };
|
|
268
|
+
}
|
|
200
269
|
/** 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.
|
|
201
270
|
* @internal
|
|
202
271
|
*/
|
|
203
|
-
export function SkyDaylight({ sky, lights = true, bands }) {
|
|
272
|
+
export function SkyDaylight({ sky, lights = true, bands, fogRange }) {
|
|
204
273
|
if (bandsDriveSkyOrFog(bands))
|
|
205
|
-
return _jsx(BiomeDaylight, { sky: sky, bands: bands, lights: lights });
|
|
206
|
-
return _jsx(StaticSkyDaylight, { sky: sky, lights: lights });
|
|
274
|
+
return _jsx(BiomeDaylight, { sky: sky, bands: bands, lights: lights, fogRange: fogRange });
|
|
275
|
+
return _jsx(StaticSkyDaylight, { sky: sky, lights: lights, fogRange: fogRange });
|
|
207
276
|
}
|
|
208
277
|
/** Dome-shape overrides (radius, haze, sun-glow) carried by the sky descriptor, spread onto `SkyDome`; unset fields keep the dome defaults. */
|
|
209
278
|
function skyDomeShape(sky) {
|
|
@@ -215,43 +284,48 @@ function skyDomeShape(sky) {
|
|
|
215
284
|
...(sky.cloudiness === undefined ? {} : { cloudiness: sky.cloudiness }),
|
|
216
285
|
};
|
|
217
286
|
}
|
|
218
|
-
function StaticSkyDaylight({ sky, lights }) {
|
|
287
|
+
function StaticSkyDaylight({ sky, lights, fogRange, }) {
|
|
219
288
|
const state = useMemo(() => daylightStateAt(SKY_PRESET_DAY_FRACTION[sky.preset], sky), [sky]);
|
|
220
|
-
|
|
289
|
+
const range = skyFogRange(sky, fogRange, { near: 70, far: 260 });
|
|
290
|
+
return (_jsx(Daylight, { sky: { topColor: state.skyTop, horizonColor: state.skyBottom, ...skyDomeShape(sky) }, fog: { color: sky.fog?.color ?? state.fogColor, ...range }, sun: { position: state.sunPosition, intensity: state.sunIntensity, color: state.sunColor }, moon: { position: state.moonPosition, intensity: state.moonIntensity, color: state.moonColor, stars: state.starOpacity }, ambient: { intensity: state.ambientIntensity }, lights: lights, clouds: sky.volumetricClouds }));
|
|
221
291
|
}
|
|
222
292
|
/**
|
|
223
293
|
* Drives sky/fog (and optional default lights) from the world clock when `sky.timeOfDay` and `clock`
|
|
224
294
|
* are both present. Authored `PlayableGame.lighting` is never rewritten — pass `lights={false}` so
|
|
225
|
-
* only dome colors and fog track the day fraction
|
|
295
|
+
* only dome colors and fog track the day fraction; `AuthoredDaylight` scales those authored lights
|
|
296
|
+
* from the same state.
|
|
226
297
|
* @internal
|
|
227
298
|
*/
|
|
228
|
-
export function TimeOfDayDaylight({ sky, clock, lights = true, bands }) {
|
|
299
|
+
export function TimeOfDayDaylight({ sky, clock, lights = true, bands, fogRange }) {
|
|
229
300
|
if (bandsDriveSkyOrFog(bands))
|
|
230
|
-
return _jsx(BiomeDaylight, { sky: sky, bands: bands, clock: clock, lights: lights });
|
|
301
|
+
return _jsx(BiomeDaylight, { sky: sky, bands: bands, clock: clock, lights: lights, fogRange: fogRange });
|
|
231
302
|
if (!sky.timeOfDay || clock === undefined)
|
|
232
|
-
return _jsx(SkyDaylight, { sky: sky, lights: lights });
|
|
233
|
-
return _jsx(DrivenDaylight, { sky: sky, clock: clock, lights: lights });
|
|
303
|
+
return _jsx(SkyDaylight, { sky: sky, lights: lights, bands: bands, fogRange: fogRange });
|
|
304
|
+
return _jsx(DrivenDaylight, { sky: sky, clock: clock, lights: lights, fogRange: fogRange });
|
|
234
305
|
}
|
|
235
306
|
/**
|
|
236
307
|
* Per-frame biome fog/sky driver: cross-fades fog color/range, dome colors, and sun/ambient intensity
|
|
237
308
|
* along the camera's z through the terrain's `biomeBands`. Base look is the sky preset (or, when
|
|
238
309
|
* `sky.timeOfDay` and a `clock` are present, the live day fraction); bands ride on top.
|
|
239
310
|
*/
|
|
240
|
-
function BiomeDaylight({ sky, bands, clock, lights, }) {
|
|
311
|
+
function BiomeDaylight({ sky, bands, clock, lights, fogRange, }) {
|
|
241
312
|
const timeOfDay = sky.timeOfDay && clock !== undefined;
|
|
242
313
|
const baseFraction = timeOfDay ? clock.calendar().dayFraction : SKY_PRESET_DAY_FRACTION[sky.preset];
|
|
243
314
|
const initial = useMemo(() => daylightStateAt(baseFraction, sky), [sky, baseFraction]);
|
|
244
315
|
const sunRef = useRef(null);
|
|
245
316
|
const sunDirectionRef = useRef(initial.sunPosition);
|
|
317
|
+
const moonRef = useRef(null);
|
|
318
|
+
const moonDirectionRef = useRef(initial.moonPosition);
|
|
246
319
|
const hemiRef = useRef(null);
|
|
247
320
|
const fogRef = useRef(null);
|
|
248
321
|
const skyMaterialRef = useRef(null);
|
|
322
|
+
const range = skyFogRange(sky, fogRange, { near: 70, far: 260 });
|
|
249
323
|
const fogFallback = useMemo(() => ({
|
|
250
|
-
color: sky.fog?.color ?? initial.
|
|
251
|
-
near:
|
|
252
|
-
far:
|
|
324
|
+
color: sky.fog?.color ?? initial.fogColor,
|
|
325
|
+
near: range.near,
|
|
326
|
+
far: range.far,
|
|
253
327
|
density: 0,
|
|
254
|
-
}), [sky.fog?.color,
|
|
328
|
+
}), [sky.fog?.color, range.near, range.far, initial.fogColor]);
|
|
255
329
|
const skyFallback = useMemo(() => ({
|
|
256
330
|
horizonColor: initial.skyBottom,
|
|
257
331
|
zenithColor: initial.skyTop,
|
|
@@ -260,14 +334,18 @@ function BiomeDaylight({ sky, bands, clock, lights, }) {
|
|
|
260
334
|
}), [initial.skyBottom, initial.skyTop, initial.sunIntensity, initial.ambientIntensity]);
|
|
261
335
|
const fogSampler = useMemo(() => createBiomeFogSampler(bands, fogFallback), [bands, fogFallback]);
|
|
262
336
|
const skySampler = useMemo(() => createBiomeSkySampler(bands, skyFallback), [bands, skyFallback]);
|
|
337
|
+
const [moonUp, syncMoon] = useMoonAboveHorizon(initial.moonIntensity > 0);
|
|
263
338
|
useFrame((state) => {
|
|
264
339
|
const base = timeOfDay ? daylightStateAt(clock.calendar().dayFraction, sky) : initial;
|
|
340
|
+
syncMoon(base.moonIntensity);
|
|
265
341
|
const z = state.camera.position.z;
|
|
266
342
|
const fog = fogSampler(z);
|
|
267
343
|
const skyValue = skySampler(z);
|
|
268
344
|
const fogNode = fogRef.current;
|
|
269
345
|
if (fogNode !== null) {
|
|
270
|
-
|
|
346
|
+
// Under a day cycle the sky owns the fog color (bands still own its range): a fixed noon
|
|
347
|
+
// tint would leave bright haze hanging in a black night sky.
|
|
348
|
+
setSkyFogColor(fogNode, sky.fog?.color ?? (timeOfDay ? base.fogColor : fog.color));
|
|
271
349
|
fogNode.near = fog.near;
|
|
272
350
|
fogNode.far = fog.far;
|
|
273
351
|
}
|
|
@@ -275,41 +353,60 @@ function BiomeDaylight({ sky, bands, clock, lights, }) {
|
|
|
275
353
|
if (skyMaterial !== null) {
|
|
276
354
|
setSkyDomeColors(skyMaterial, skyValue.zenithColor, skyValue.horizonColor);
|
|
277
355
|
aimSkySun(skyMaterial, base.sunPosition);
|
|
356
|
+
setSkyMoon(skyMaterial, base.moonPosition, base.moonColor, base.moonIntensity, base.starOpacity);
|
|
278
357
|
}
|
|
279
358
|
sunDirectionRef.current = base.sunPosition;
|
|
359
|
+
moonDirectionRef.current = base.moonPosition;
|
|
360
|
+
const moon = moonRef.current;
|
|
361
|
+
if (moon !== null)
|
|
362
|
+
moon.intensity = base.moonIntensity;
|
|
280
363
|
const sun = sunRef.current;
|
|
281
|
-
if (sun !== null)
|
|
364
|
+
if (sun !== null) {
|
|
282
365
|
sun.intensity = skyValue.sunIntensity;
|
|
366
|
+
sun.color.set(base.sunColor);
|
|
367
|
+
}
|
|
283
368
|
const hemi = hemiRef.current;
|
|
284
369
|
if (hemi !== null)
|
|
285
370
|
hemi.intensity = skyValue.ambientIntensity;
|
|
286
371
|
});
|
|
287
|
-
return (_jsxs(_Fragment, { children: [_jsx(SkyDome, { topColor: initial.skyTop, horizonColor: initial.skyBottom, sunDirection: initial.sunPosition, sunColor:
|
|
372
|
+
return (_jsxs(_Fragment, { children: [_jsx(SkyDome, { topColor: initial.skyTop, horizonColor: initial.skyBottom, sunDirection: initial.sunPosition, sunColor: initial.sunColor, materialRef: skyMaterialRef, moonDirection: initial.moonPosition, moonColor: initial.moonColor, moonIntensity: initial.moonIntensity, starOpacity: initial.starOpacity, ...skyDomeShape(sky) }), sky.volumetricClouds === undefined ? null : (_jsx(VolumetricClouds, { rules: resolveVolumetricClouds(sky.volumetricClouds), sunDirection: initial.sunPosition })), _jsx(SkyFog, { fogRef: fogRef, color: fogFallback.color, near: fogFallback.near, far: fogFallback.far }), lights ? (_jsxs(_Fragment, { children: [_jsx("hemisphereLight", { ref: hemiRef, args: [HEMI_SKY, HEMI_GROUND, initial.ambientIntensity] }), _jsx(SunLight, { direction: initial.sunPosition, directionRef: sunDirectionRef, lightRef: sunRef, intensity: initial.sunIntensity, color: initial.sunColor }), moonUp ? (_jsx(MoonLight, { direction: initial.moonPosition, directionRef: moonDirectionRef, lightRef: moonRef, intensity: initial.moonIntensity, color: initial.moonColor })) : null] })) : null] }));
|
|
288
373
|
}
|
|
289
|
-
function DrivenDaylight({ sky, clock, lights, }) {
|
|
374
|
+
function DrivenDaylight({ sky, clock, lights, fogRange, }) {
|
|
290
375
|
const initial = useMemo(() => daylightStateAt(clock.calendar().dayFraction, sky), [clock, sky]);
|
|
291
376
|
const sunRef = useRef(null);
|
|
292
377
|
const sunDirectionRef = useRef(initial.sunPosition);
|
|
378
|
+
const moonRef = useRef(null);
|
|
379
|
+
const moonDirectionRef = useRef(initial.moonPosition);
|
|
293
380
|
const hemiRef = useRef(null);
|
|
294
381
|
const fogRef = useRef(null);
|
|
295
382
|
const skyMaterialRef = useRef(null);
|
|
383
|
+
const range = skyFogRange(sky, fogRange, { near: 70, far: 260 });
|
|
384
|
+
const [moonUp, syncMoon] = useMoonAboveHorizon(initial.moonIntensity > 0);
|
|
296
385
|
useFrame(() => {
|
|
297
386
|
const state = daylightStateAt(clock.calendar().dayFraction, sky);
|
|
298
387
|
sunDirectionRef.current = state.sunPosition;
|
|
388
|
+
moonDirectionRef.current = state.moonPosition;
|
|
389
|
+
syncMoon(state.moonIntensity);
|
|
299
390
|
const sun = sunRef.current;
|
|
300
|
-
if (sun !== null)
|
|
391
|
+
if (sun !== null) {
|
|
301
392
|
sun.intensity = state.sunIntensity;
|
|
393
|
+
sun.color.set(state.sunColor);
|
|
394
|
+
}
|
|
395
|
+
const moon = moonRef.current;
|
|
396
|
+
if (moon !== null)
|
|
397
|
+
moon.intensity = state.moonIntensity;
|
|
302
398
|
const hemi = hemiRef.current;
|
|
303
399
|
if (hemi !== null)
|
|
304
400
|
hemi.intensity = state.ambientIntensity;
|
|
305
401
|
const fog = fogRef.current;
|
|
306
402
|
if (fog !== null)
|
|
307
|
-
setSkyFogColor(fog, sky.fog?.color ?? state.
|
|
403
|
+
setSkyFogColor(fog, sky.fog?.color ?? state.fogColor);
|
|
308
404
|
const skyMaterial = skyMaterialRef.current;
|
|
309
405
|
if (skyMaterial !== null) {
|
|
310
406
|
setSkyDomeColors(skyMaterial, state.skyTop, state.skyBottom);
|
|
311
407
|
aimSkySun(skyMaterial, state.sunPosition);
|
|
408
|
+
setSkyMoon(skyMaterial, state.moonPosition, state.moonColor, state.moonIntensity, state.starOpacity);
|
|
312
409
|
}
|
|
313
410
|
});
|
|
314
|
-
return (_jsxs(_Fragment, { children: [_jsx(SkyDome, { topColor: initial.skyTop, horizonColor: initial.skyBottom, sunDirection: initial.sunPosition, sunColor:
|
|
411
|
+
return (_jsxs(_Fragment, { children: [_jsx(SkyDome, { topColor: initial.skyTop, horizonColor: initial.skyBottom, sunDirection: initial.sunPosition, sunColor: initial.sunColor, materialRef: skyMaterialRef, moonDirection: initial.moonPosition, moonColor: initial.moonColor, moonIntensity: initial.moonIntensity, starOpacity: initial.starOpacity, ...skyDomeShape(sky) }), sky.volumetricClouds === undefined ? null : (_jsx(VolumetricClouds, { rules: resolveVolumetricClouds(sky.volumetricClouds), sunDirection: initial.sunPosition })), _jsx(SkyFog, { fogRef: fogRef, color: sky.fog?.color ?? initial.fogColor, near: range.near, far: range.far }), lights ? (_jsxs(_Fragment, { children: [_jsx("hemisphereLight", { ref: hemiRef, args: [HEMI_SKY, HEMI_GROUND, initial.ambientIntensity] }), _jsx(SunLight, { direction: initial.sunPosition, directionRef: sunDirectionRef, lightRef: sunRef, intensity: initial.sunIntensity, color: initial.sunColor }), moonUp ? (_jsx(MoonLight, { direction: initial.moonPosition, directionRef: moonDirectionRef, lightRef: moonRef, intensity: initial.moonIntensity, color: initial.moonColor })) : null] })) : null] }));
|
|
315
412
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
3
3
|
import { useFrame } from "@react-three/fiber";
|
|
4
4
|
import { PresentationAssetBoundary } from "../presentationRecovery.js";
|
|
5
5
|
import { PresentationRecoveredDraw, useRecoverableTexture } from "../presentationRecovery3d.js";
|
|
@@ -18,14 +18,16 @@ import { buildAuthoredRoadNetwork, RoadRibbons } from "./RoadRibbons.js";
|
|
|
18
18
|
import { terrainGroundColorSampler } from "./terrainGroundColor.js";
|
|
19
19
|
import { InstancedBuildings } from "../structures/GeneratedBuilding.js";
|
|
20
20
|
import { GrassField } from "../terrain/GrassField.js";
|
|
21
|
+
import { GrassCover } from "../terrain/GrassCover.js";
|
|
21
22
|
import { CarvedTerrain } from "../terrain/CarvedTerrain.js";
|
|
22
23
|
import { createTerrainDetailMaterial } from "../terrain/terrainDetailMaterial.js";
|
|
24
|
+
import { MAX_TERRAIN_LAYER_MAPS } from "../terrain/terrainDetailMaterial.js";
|
|
23
25
|
import { useTerrainQuality } from "../terrain/terrainQuality.js";
|
|
24
26
|
import { createTerrainPaintTextures, packTerrainPaintWeights } from "../terrain/terrainSurfaces.js";
|
|
25
27
|
import { Ocean } from "../water/Ocean.js";
|
|
26
28
|
import { RainField } from "../weather/RainField.js";
|
|
27
29
|
import { SnowField } from "../weather/SnowField.js";
|
|
28
|
-
function TexturedTerrainGround({ detail, layout, }) {
|
|
30
|
+
function TexturedTerrainGround({ detail, layout, layerMaps, }) {
|
|
29
31
|
const maps = detail.material.maps;
|
|
30
32
|
const textures = useRecoverableTexture({
|
|
31
33
|
color: maps.color,
|
|
@@ -42,9 +44,44 @@ function TexturedTerrainGround({ detail, layout, }) {
|
|
|
42
44
|
}
|
|
43
45
|
}, [textures]);
|
|
44
46
|
const quality = useTerrainQuality();
|
|
45
|
-
const material = useDisposable(() => createTerrainDetailMaterial(detail, textures, { paint: layout.paint, quality }).material, [detail, textures, layout.paint, quality]);
|
|
47
|
+
const material = useDisposable(() => createTerrainDetailMaterial(detail, textures, { paint: layout.paint, quality, ...(layerMaps === undefined ? {} : { layerMaps }) }).material, [detail, textures, layout.paint, quality, layerMaps]);
|
|
46
48
|
return (_jsxs(_Fragment, { children: [_jsx(PresentationRecoveredDraw, {}), _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 })] }));
|
|
47
49
|
}
|
|
50
|
+
/** 1×1 white PNG: binds a valid sampler for a painted layer that has no scanned map. */
|
|
51
|
+
const NO_LAYER_MAP = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
|
|
52
|
+
/**
|
|
53
|
+
* Scanned color maps for up to {@link MAX_TERRAIN_LAYER_MAPS} painted terrain layers, loaded through
|
|
54
|
+
* the shared recoverable texture path and tiled at each layer's own authored metres. The URL is
|
|
55
|
+
* authored data (`TerrainMaterialLayer.materialUrl`), resolved by the editor from a library name, so
|
|
56
|
+
* the renderer never reaches into the asset pipeline.
|
|
57
|
+
*/
|
|
58
|
+
function useTerrainLayerMaps(paint) {
|
|
59
|
+
const urls = useMemo(() => Array.from({ length: MAX_TERRAIN_LAYER_MAPS }, (_, index) => {
|
|
60
|
+
const url = paint?.slots[index + 1]?.materialUrl;
|
|
61
|
+
return url === undefined || url.length === 0 ? NO_LAYER_MAP : url;
|
|
62
|
+
}), [paint]);
|
|
63
|
+
const textures = useRecoverableTexture({ layer1: urls[0], layer2: urls[1], layer3: urls[2], layer4: urls[3] });
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
for (const texture of Object.values(textures)) {
|
|
66
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
67
|
+
texture.wrapS = THREE.RepeatWrapping;
|
|
68
|
+
texture.wrapT = THREE.RepeatWrapping;
|
|
69
|
+
texture.needsUpdate = true;
|
|
70
|
+
}
|
|
71
|
+
}, [textures]);
|
|
72
|
+
const slots = paint?.slots;
|
|
73
|
+
return useMemo(() => {
|
|
74
|
+
if (slots === undefined)
|
|
75
|
+
return undefined;
|
|
76
|
+
return Array.from({ length: MAX_TERRAIN_LAYER_MAPS }, (_, index) => {
|
|
77
|
+
const slot = slots[index + 1];
|
|
78
|
+
if (slot?.materialUrl === undefined)
|
|
79
|
+
return undefined;
|
|
80
|
+
const texture = textures[`layer${index + 1}`];
|
|
81
|
+
return texture === undefined ? undefined : { color: texture, tileMetres: slot.tileMetres };
|
|
82
|
+
});
|
|
83
|
+
}, [slots, textures]);
|
|
84
|
+
}
|
|
48
85
|
function TerrainGround({ terrain, field, center, sculpt, }) {
|
|
49
86
|
const detailEnabled = terrain.detail !== false;
|
|
50
87
|
const paint = useMemo(() => {
|
|
@@ -79,12 +116,13 @@ function TerrainGround({ terrain, field, center, sculpt, }) {
|
|
|
79
116
|
}, [terrain.baseHeight, terrain.height]);
|
|
80
117
|
const resolvedDetail = useMemo(() => (terrain.detail === false ? undefined : resolveTerrainDetail(terrain.detail, terrain.waterLevel)), [terrain.detail, terrain.waterLevel]);
|
|
81
118
|
const quality = useTerrainQuality();
|
|
119
|
+
const layerMaps = useTerrainLayerMaps(paint);
|
|
82
120
|
const proceduralMaterial = useMemo(() => resolvedDetail === undefined || resolvedDetail.material !== undefined
|
|
83
121
|
? undefined
|
|
84
|
-
: createTerrainDetailMaterial(resolvedDetail, undefined, { paint, quality }).material, [resolvedDetail, paint, quality]);
|
|
122
|
+
: createTerrainDetailMaterial(resolvedDetail, undefined, { paint, quality, ...(layerMaps === undefined ? {} : { layerMaps }) }).material, [resolvedDetail, paint, quality, layerMaps]);
|
|
85
123
|
useEffect(() => () => proceduralMaterial?.dispose(), [proceduralMaterial]);
|
|
86
124
|
if (resolvedDetail?.material !== undefined) {
|
|
87
|
-
return (_jsx(PresentationAssetBoundary, { children: _jsx(TexturedTerrainGround, { detail: resolvedDetail, layout: { field, size, segments: terrain.segments, colors, heightRange, paletteAt, paint, center } }) }));
|
|
125
|
+
return (_jsx(PresentationAssetBoundary, { children: _jsx(TexturedTerrainGround, { detail: resolvedDetail, layout: { field, size, segments: terrain.segments, colors, heightRange, paletteAt, paint, center }, ...(layerMaps === undefined ? {} : { layerMaps }) }) }));
|
|
88
126
|
}
|
|
89
127
|
return (_jsx(CarvedTerrain, { field: field, size: size, segments: terrain.segments, colors: colors, heightRange: heightRange, paletteAt: paletteAt, paintColorAt: paintColorAt, center: center, roughness: 0.94, surfaceMaterial: proceduralMaterial }));
|
|
90
128
|
}
|
|
@@ -100,7 +138,7 @@ function Vegetation({ grass, field, groundColor, }) {
|
|
|
100
138
|
const wind = useMemo(() => ({ strength: grass.windStrength }), [grass.windStrength]);
|
|
101
139
|
const colorBase = grass.colors[0];
|
|
102
140
|
const colorTip = grass.colors[grass.colors.length - 1];
|
|
103
|
-
return (
|
|
141
|
+
return (_jsxs("group", { position: [cx, 0, cz], children: [_jsx(GrassCover, { area: area, seed: typeof grass.seed === "number" ? grass.seed : 1, colorBase: colorBase ?? "#3f7d2d", colorTip: colorTip ?? "#66a83f", strength: grass.groundCover ?? 0.9, edgeFeather: Math.min(8, Math.max(1.5, Math.min(grass.area.w, grass.area.d) * 0.12)), heightAt: heightAt, coverageAt: () => 1 }), _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, bladeBend: grass.bladeBend, tuftRadius: grass.tuftRadius, colorVariation: grass.colorVariation, normalLift: grass.normalLift, 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 })] }));
|
|
104
142
|
}
|
|
105
143
|
/** Terrain color under a grass patch center — so blade roots blend into the ground they stand on. */
|
|
106
144
|
function vegetationGroundColor(terrain, field, grass, sculpt) {
|
|
@@ -142,19 +180,24 @@ function DynamicWater({ ocean }) {
|
|
|
142
180
|
const groupRef = useRef(null);
|
|
143
181
|
const [x, z] = ocean.position ?? [0, 0];
|
|
144
182
|
const config = useOceanConfig(ocean);
|
|
183
|
+
const depthAt = useCallback((lx, lz) => ocean.levelAt(ctx.time.now()) - ctx.world.ground.sampleHeight(x + lx, z + lz), [ctx.time.now, ctx.world.ground, ocean.levelAt, x, z]);
|
|
145
184
|
useFrame(() => {
|
|
146
185
|
if (groupRef.current !== null)
|
|
147
186
|
groupRef.current.position.y = ocean.levelAt(ctx.time.now());
|
|
148
187
|
});
|
|
149
|
-
return (_jsx("group", { ref: groupRef, position: [0, ocean.level, 0], children: _jsx(Ocean, { position: [x, 0, z], config: config }) }));
|
|
188
|
+
return (_jsx("group", { ref: groupRef, position: [0, ocean.level, 0], children: _jsx(Ocean, { position: [x, 0, z], config: config, depthAt: depthAt }) }));
|
|
150
189
|
}
|
|
151
190
|
function Water({ ocean }) {
|
|
191
|
+
const ctx = useGameContext();
|
|
152
192
|
const [x, z] = ocean.position ?? [0, 0];
|
|
153
193
|
const config = useOceanConfig(ocean);
|
|
194
|
+
// The ocean descriptor has no depth field: sample the live ground so the sheet shades
|
|
195
|
+
// shallow→deep and the shoreline foam band lands where the bed meets the surface.
|
|
196
|
+
const depthAt = useCallback((lx, lz) => ocean.level - ctx.world.ground.sampleHeight(x + lx, z + lz), [ctx.world.ground, ocean.level, x, z]);
|
|
154
197
|
if (ocean.levelAt !== undefined) {
|
|
155
198
|
return _jsx(DynamicWater, { ocean: ocean });
|
|
156
199
|
}
|
|
157
|
-
return _jsx(Ocean, { position: [x, ocean.level, z], config: config });
|
|
200
|
+
return _jsx(Ocean, { position: [x, ocean.level, z], config: config, depthAt: depthAt });
|
|
158
201
|
}
|
|
159
202
|
function Structures({ structures, field }) {
|
|
160
203
|
const placements = useMemo(() => resolveStructureBuildings(structures).map((building) => ({
|
|
@@ -6,19 +6,40 @@ export interface DaylightCycleConfig {
|
|
|
6
6
|
ambientIntensity?: number;
|
|
7
7
|
/** Fixed sun placement, or the noon placement the day arc swings around. Unset keeps the engine arc. */
|
|
8
8
|
sun?: SkySunConfig;
|
|
9
|
+
/** Authored noon sun colour, reddened toward the horizon by the atmosphere. Default warm white. */
|
|
10
|
+
sunColor?: string;
|
|
11
|
+
/** Authored noon fog colour; dawn, dusk and night cross-fade to their own haze. Default the noon horizon. */
|
|
12
|
+
fogColor?: string;
|
|
13
|
+
/** Peak moonlight as a fraction of `sunIntensity`. Default `MOONLIGHT_PEAK`. */
|
|
14
|
+
moonIntensity?: number;
|
|
15
|
+
/** Moonlight colour. Default `MOONLIGHT_COLOR`. */
|
|
16
|
+
moonColor?: string;
|
|
9
17
|
}
|
|
10
18
|
export interface DaylightState {
|
|
11
19
|
sunPosition: [number, number, number];
|
|
12
20
|
sunIntensity: number;
|
|
21
|
+
/** The sun's colour at this hour: authored tint high up, atmosphere-reddened near the horizon. */
|
|
22
|
+
sunColor: string;
|
|
13
23
|
ambientIntensity: number;
|
|
14
24
|
skyTop: string;
|
|
15
25
|
skyBottom: string;
|
|
16
26
|
background: string;
|
|
27
|
+
/** Scene fog colour matched to the sky at this hour, so fogged ground meets the horizon. */
|
|
28
|
+
fogColor: string;
|
|
29
|
+
/** The moon's direction — the sun's arc half a day away, so it is up whenever the sun is down. */
|
|
30
|
+
moonPosition: [number, number, number];
|
|
31
|
+
/** Moonlight intensity, already scaled by the authored noon sun intensity. */
|
|
32
|
+
moonIntensity: number;
|
|
33
|
+
moonColor: string;
|
|
34
|
+
/** Star visibility, 0 in daylight and 1 with the sun well under the horizon. */
|
|
35
|
+
starOpacity: number;
|
|
17
36
|
}
|
|
18
37
|
export declare const DEFAULT_DAY_SUN_INTENSITY = 1;
|
|
19
38
|
export declare const DEFAULT_DAY_AMBIENT_INTENSITY = 0.6;
|
|
20
39
|
export declare const DEFAULT_DAY_SKY_TOP = "#3fa4f2";
|
|
21
40
|
export declare const DEFAULT_DAY_SKY_BOTTOM = "#e3f4ff";
|
|
41
|
+
/** Warm-white noon sun used when the game authored no sun tint (the shell's `SUN_COLOR`). */
|
|
42
|
+
export declare const DEFAULT_SUN_COLOR = "#fff1c9";
|
|
22
43
|
/** The engine arc's noon height: `atan(1 / SUN_DEPTH_RATIO)`, ~68 degrees. */
|
|
23
44
|
export declare const DEFAULT_SUN_ELEVATION_DEG: number;
|
|
24
45
|
/**
|
|
@@ -41,8 +62,9 @@ export declare const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", n
|
|
|
41
62
|
export declare function lerpHexColor(a: string, b: string, t: number): string;
|
|
42
63
|
/**
|
|
43
64
|
* Samples the daylight cycle at a point in the day (0 = midnight, 0.25 = dawn, 0.5 = noon, 0.75 = dusk),
|
|
44
|
-
* lerping sun position/intensity, ambient intensity, and
|
|
45
|
-
* keyframe table
|
|
46
|
-
|
|
47
|
-
|
|
65
|
+
* lerping sun position/intensity, ambient intensity, sky and fog colors through a dawn/day/dusk/night
|
|
66
|
+
* keyframe table, and deriving sun tint, moonlight and star visibility from the sun's elevation.
|
|
67
|
+
* `config` overrides the noon (peak-day) colors and intensities only.
|
|
68
|
+
* @internal
|
|
69
|
+
*/
|
|
48
70
|
export declare function daylightStateAt(dayFraction: number, config?: DaylightCycleConfig): DaylightState;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { elevationOf, MOONLIGHT_COLOR, moonIllumination, NIGHT_FOG_COLOR, starOpacity, sunlightColor, } from "@jgengine/core/render/physicalSky";
|
|
1
2
|
const NIGHT_SUN_INTENSITY = 0.02;
|
|
2
3
|
const NIGHT_AMBIENT_INTENSITY = 0.08;
|
|
3
4
|
const NIGHT_SKY_TOP = "#02030a";
|
|
@@ -14,6 +15,8 @@ export const DEFAULT_DAY_SUN_INTENSITY = 1;
|
|
|
14
15
|
export const DEFAULT_DAY_AMBIENT_INTENSITY = 0.6;
|
|
15
16
|
export const DEFAULT_DAY_SKY_TOP = "#3fa4f2";
|
|
16
17
|
export const DEFAULT_DAY_SKY_BOTTOM = "#e3f4ff";
|
|
18
|
+
/** Warm-white noon sun used when the game authored no sun tint (the shell's `SUN_COLOR`). */
|
|
19
|
+
export const DEFAULT_SUN_COLOR = "#fff1c9";
|
|
17
20
|
const SUN_DISTANCE = 200;
|
|
18
21
|
const SUN_DEPTH_RATIO = 0.4;
|
|
19
22
|
const DEG = Math.PI / 180;
|
|
@@ -79,6 +82,7 @@ function buildKeyframes(config) {
|
|
|
79
82
|
ambientIntensity: NIGHT_AMBIENT_INTENSITY,
|
|
80
83
|
skyTop: NIGHT_SKY_TOP,
|
|
81
84
|
skyBottom: NIGHT_SKY_BOTTOM,
|
|
85
|
+
fog: NIGHT_FOG_COLOR,
|
|
82
86
|
};
|
|
83
87
|
const dawn = {
|
|
84
88
|
t: 0.25,
|
|
@@ -86,6 +90,7 @@ function buildKeyframes(config) {
|
|
|
86
90
|
ambientIntensity: DAWN_AMBIENT_INTENSITY,
|
|
87
91
|
skyTop: DAWN_SKY_TOP,
|
|
88
92
|
skyBottom: DAWN_SKY_BOTTOM,
|
|
93
|
+
fog: DAWN_SKY_BOTTOM,
|
|
89
94
|
};
|
|
90
95
|
const day = {
|
|
91
96
|
t: 0.5,
|
|
@@ -93,6 +98,7 @@ function buildKeyframes(config) {
|
|
|
93
98
|
ambientIntensity: config.ambientIntensity ?? DEFAULT_DAY_AMBIENT_INTENSITY,
|
|
94
99
|
skyTop: config.zenithColor ?? DEFAULT_DAY_SKY_TOP,
|
|
95
100
|
skyBottom: config.horizonColor ?? DEFAULT_DAY_SKY_BOTTOM,
|
|
101
|
+
fog: config.fogColor ?? config.horizonColor ?? DEFAULT_DAY_SKY_BOTTOM,
|
|
96
102
|
};
|
|
97
103
|
const dusk = {
|
|
98
104
|
t: 0.75,
|
|
@@ -100,6 +106,7 @@ function buildKeyframes(config) {
|
|
|
100
106
|
ambientIntensity: DUSK_AMBIENT_INTENSITY,
|
|
101
107
|
skyTop: DUSK_SKY_TOP,
|
|
102
108
|
skyBottom: DUSK_SKY_BOTTOM,
|
|
109
|
+
fog: DUSK_SKY_BOTTOM,
|
|
103
110
|
};
|
|
104
111
|
const midnight = { ...night, t: 1 };
|
|
105
112
|
return [night, dawn, day, dusk, midnight];
|
|
@@ -122,10 +129,11 @@ function segmentAt(wrapped, keyframes) {
|
|
|
122
129
|
}
|
|
123
130
|
/**
|
|
124
131
|
* Samples the daylight cycle at a point in the day (0 = midnight, 0.25 = dawn, 0.5 = noon, 0.75 = dusk),
|
|
125
|
-
* lerping sun position/intensity, ambient intensity, and
|
|
126
|
-
* keyframe table
|
|
127
|
-
|
|
128
|
-
|
|
132
|
+
* lerping sun position/intensity, ambient intensity, sky and fog colors through a dawn/day/dusk/night
|
|
133
|
+
* keyframe table, and deriving sun tint, moonlight and star visibility from the sun's elevation.
|
|
134
|
+
* `config` overrides the noon (peak-day) colors and intensities only.
|
|
135
|
+
* @internal
|
|
136
|
+
*/
|
|
129
137
|
export function daylightStateAt(dayFraction, config = {}) {
|
|
130
138
|
const wrapped = wrapFraction(dayFraction);
|
|
131
139
|
const keyframes = buildKeyframes(config);
|
|
@@ -134,8 +142,31 @@ export function daylightStateAt(dayFraction, config = {}) {
|
|
|
134
142
|
const ambientIntensity = lerp(from.ambientIntensity, to.ambientIntensity, localT);
|
|
135
143
|
const skyTop = lerpHexColor(from.skyTop, to.skyTop, localT);
|
|
136
144
|
const skyBottom = lerpHexColor(from.skyBottom, to.skyBottom, localT);
|
|
145
|
+
const fogColor = lerpHexColor(from.fog, to.fog, localT);
|
|
137
146
|
const sunPosition = sunPositionAt(wrapped, config.sun);
|
|
138
|
-
|
|
147
|
+
const moonPosition = sunPositionAt(wrapped + 0.5, config.sun);
|
|
148
|
+
const sunColor = mixHex(config.sunColor ?? DEFAULT_SUN_COLOR, sunlightColor(elevationOf(sunPosition)), lowSunWeight(wrapped));
|
|
149
|
+
return {
|
|
150
|
+
sunPosition,
|
|
151
|
+
sunIntensity,
|
|
152
|
+
sunColor,
|
|
153
|
+
ambientIntensity,
|
|
154
|
+
skyTop,
|
|
155
|
+
skyBottom,
|
|
156
|
+
background: skyBottom,
|
|
157
|
+
fogColor,
|
|
158
|
+
moonPosition,
|
|
159
|
+
moonIntensity: moonIllumination(elevationOf(moonPosition), config.moonIntensity) * (config.sunIntensity ?? DEFAULT_DAY_SUN_INTENSITY),
|
|
160
|
+
moonColor: config.moonColor ?? MOONLIGHT_COLOR,
|
|
161
|
+
starOpacity: starOpacity(elevationOf(sunPosition)),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function mixHex(from, to, t) {
|
|
165
|
+
return t <= 0 ? from : t >= 1 ? to : lerpHexColor(from, to, t);
|
|
166
|
+
}
|
|
167
|
+
/** How much of the sun's colour comes from the atmosphere rather than the authored tint: 0 with the sun high, 1 once it is on the horizon. */
|
|
168
|
+
function lowSunWeight(wrapped) {
|
|
169
|
+
return Math.min(1, Math.abs(wrapped - 0.5) / 0.25);
|
|
139
170
|
}
|
|
140
171
|
/**
|
|
141
172
|
* Sun position for a wrapped day fraction. Without `sun`, the legacy arc: east at dawn, ~68 degrees
|
|
@@ -5,6 +5,12 @@ interface BuiltGraph {
|
|
|
5
5
|
composer: EffectComposer;
|
|
6
6
|
grade: ShaderPass | null;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Rounds an adapted exposure to 1/32 of a stop. Without it the sky's display-colour cache (which keys
|
|
10
|
+
* off the renderer's exposure) re-solves its swatches every frame the meter nudges.
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export declare function quantizeExposure(value: number): number;
|
|
8
14
|
/** Release each pass and the composer's owned targets. @internal */
|
|
9
15
|
export declare function disposeGraph(built: BuiltGraph): void;
|
|
10
16
|
/** Resize the composer and its passes at the renderer's pixel ratio. @internal */
|
|
@@ -14,7 +20,7 @@ export declare function resolveAoStage(ao: PostProcessingConfig["ao"], tierAllow
|
|
|
14
20
|
/**
|
|
15
21
|
* Mounts an `EffectComposer` inside the shell Canvas and takes over rendering
|
|
16
22
|
* (priority-1 `useFrame`, which disables R3F auto-render) to run the configured
|
|
17
|
-
* post chain: RenderPass → GTAO → UnrealBloom → SMAA → OutputPass → Stylize → Grade. Rendered only
|
|
23
|
+
* post chain: RenderPass → GTAO → UnrealBloom → SMAA → exposure meter → OutputPass → Stylize → Grade. Rendered only
|
|
18
24
|
* when `PlayableGame.postProcessing` is set, so games without it draw unchanged.
|
|
19
25
|
*
|
|
20
26
|
* `stages` (resolved from the player's graphics profile) controls expensive passes
|