@jgengine/shell 0.24.0 → 0.26.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 +92 -0
- package/dist/Shell3dPresentation.js +29 -11
- package/dist/ShellChrome.js +1 -1
- package/dist/ShellNotices.d.ts +4 -4
- package/dist/ShellNotices.js +9 -6
- 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/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/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/settings/appliedSettings.js +16 -13
- package/package.json +4 -4
- package/skills/jgengine/api.md +59 -6
- package/skills/jgengine/capabilities.md +4 -0
- package/skills/jgengine-ui/SKILL.md +3 -3
- package/skills/jgengine-ui/api.md +98 -31
- package/skills/jgengine-ui/capabilities.md +15 -1
- package/skills/jgengine-ui/reference-primitives.md +5 -3
- package/skills/jgengine-world/SKILL.md +3 -1
- package/skills/jgengine-world/api.md +107 -3
- package/skills/jgengine-world/capabilities.md +22 -2
- package/skills/jgengine-world/recipes/npc-senses.md +8 -2
|
@@ -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
|
}
|
|
@@ -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
|
|
@@ -7,6 +7,8 @@ import { GTAOPass } from "three/examples/jsm/postprocessing/GTAOPass.js";
|
|
|
7
7
|
import { BokehPass } from "three/examples/jsm/postprocessing/BokehPass.js";
|
|
8
8
|
import { SMAAPass } from "three/examples/jsm/postprocessing/SMAAPass.js";
|
|
9
9
|
import { DEFAULT_AO } from "@jgengine/core/render/postProcessing";
|
|
10
|
+
import { adaptExposure, DEFAULT_AUTO_EXPOSURE, exposureForKey, keyLuminanceFromSamples } from "@jgengine/core/render/colorPipeline";
|
|
11
|
+
import { ExposureMeterPass } from "./autoExposure.js";
|
|
10
12
|
import { createGradePass } from "./gradeShader.js";
|
|
11
13
|
import { StylizePass } from "./stylizePass.js";
|
|
12
14
|
import { renderShadowlessPrepass } from "./shadowPrepass.js";
|
|
@@ -38,6 +40,14 @@ const TONE_MAPPING = {
|
|
|
38
40
|
linear: THREE.LinearToneMapping,
|
|
39
41
|
none: THREE.NoToneMapping,
|
|
40
42
|
};
|
|
43
|
+
/**
|
|
44
|
+
* Rounds an adapted exposure to 1/32 of a stop. Without it the sky's display-colour cache (which keys
|
|
45
|
+
* off the renderer's exposure) re-solves its swatches every frame the meter nudges.
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export function quantizeExposure(value) {
|
|
49
|
+
return Math.round(value * 32) / 32;
|
|
50
|
+
}
|
|
41
51
|
/** Release each pass and the composer's owned targets. @internal */
|
|
42
52
|
export function disposeGraph(built) {
|
|
43
53
|
for (const pass of built.composer.passes)
|
|
@@ -58,7 +68,7 @@ export function resolveAoStage(ao, tierAllowsAo) {
|
|
|
58
68
|
/**
|
|
59
69
|
* Mounts an `EffectComposer` inside the shell Canvas and takes over rendering
|
|
60
70
|
* (priority-1 `useFrame`, which disables R3F auto-render) to run the configured
|
|
61
|
-
* post chain: RenderPass → GTAO → UnrealBloom → SMAA → OutputPass → Stylize → Grade. Rendered only
|
|
71
|
+
* post chain: RenderPass → GTAO → UnrealBloom → SMAA → exposure meter → OutputPass → Stylize → Grade. Rendered only
|
|
62
72
|
* when `PlayableGame.postProcessing` is set, so games without it draw unchanged.
|
|
63
73
|
*
|
|
64
74
|
* `stages` (resolved from the player's graphics profile) controls expensive passes
|
|
@@ -113,6 +123,22 @@ export function PostProcessing({ config, quality = "high", stages }) {
|
|
|
113
123
|
if (resolvedStages.smaa && aa === "smaa") {
|
|
114
124
|
composer.addPass(new SMAAPass());
|
|
115
125
|
}
|
|
126
|
+
// Clamped auto-exposure meters the pre-tone-map buffer, so the meter sits before OutputPass and
|
|
127
|
+
// hands the adaptation to the renderer's exposure — the one value the sky swatches also read.
|
|
128
|
+
const autoExposure = config.autoExposure === false ? null : { ...DEFAULT_AUTO_EXPOSURE, ...(config.autoExposure ?? {}) };
|
|
129
|
+
const authoredExposure = config.exposure ?? 1;
|
|
130
|
+
if (autoExposure !== null) {
|
|
131
|
+
let current = authoredExposure;
|
|
132
|
+
let sampled = false;
|
|
133
|
+
composer.addPass(new ExposureMeterPass((samples, seconds) => {
|
|
134
|
+
const target = authoredExposure * exposureForKey(keyLuminanceFromSamples(samples), autoExposure);
|
|
135
|
+
// The first sample lands on its own target; fading in from the authored exposure would
|
|
136
|
+
// open every load on a visible exposure slideshow.
|
|
137
|
+
current = sampled ? adaptExposure(current, target, seconds, autoExposure.adaptationSeconds) : target;
|
|
138
|
+
sampled = true;
|
|
139
|
+
gl.toneMappingExposure = quantizeExposure(current);
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
116
142
|
composer.addPass(new OutputPass());
|
|
117
143
|
if (config.stylize !== undefined && config.stylize !== false) {
|
|
118
144
|
composer.addPass(new StylizePass(scene, camera, config.stylize));
|
|
@@ -133,7 +159,7 @@ export function PostProcessing({ config, quality = "high", stages }) {
|
|
|
133
159
|
useEffect(() => {
|
|
134
160
|
const prevTone = gl.toneMapping;
|
|
135
161
|
const prevExposure = gl.toneMappingExposure;
|
|
136
|
-
gl.toneMapping = TONE_MAPPING[config.toneMapping ?? "
|
|
162
|
+
gl.toneMapping = TONE_MAPPING[config.toneMapping ?? "agx"];
|
|
137
163
|
gl.toneMappingExposure = config.exposure ?? 1;
|
|
138
164
|
return () => {
|
|
139
165
|
gl.toneMapping = prevTone;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { Pass } from "three-stdlib";
|
|
3
|
+
/** Linear luminance encoded in one byte by {@link ExposureMeterPass}. @internal */
|
|
4
|
+
export declare function decodeMeterByte(value: number, target: Float32Array, targetIndex: number): void;
|
|
5
|
+
/**
|
|
6
|
+
* Meters the pre-tone-map beauty buffer for auto-exposure. Every `sampleInterval` frames it reduces
|
|
7
|
+
* the frame to a 16×16 grid of log-luminance (GPU) and reads that back (1 KB), then hands the decoded
|
|
8
|
+
* samples to `onSample` with the wall time since the previous sample. The pass itself is a pass-through,
|
|
9
|
+
* so it can sit anywhere before the tone-mapping output pass without changing the picture.
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare class ExposureMeterPass extends Pass {
|
|
13
|
+
private readonly onSample;
|
|
14
|
+
/** Frames between readbacks once warmed up. Reading the GPU back stalls the pipeline, so this is
|
|
15
|
+
* as rare as exposure adaptation tolerates. */
|
|
16
|
+
private readonly sampleInterval;
|
|
17
|
+
readonly meterTarget: THREE.WebGLRenderTarget;
|
|
18
|
+
readonly reduceUniforms: {
|
|
19
|
+
tDiffuse: {
|
|
20
|
+
value: THREE.Texture | null;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
private readonly copyUniforms;
|
|
24
|
+
private readonly reduceQuad;
|
|
25
|
+
private readonly copyQuad;
|
|
26
|
+
private readonly pixels;
|
|
27
|
+
private readonly samples;
|
|
28
|
+
private frames;
|
|
29
|
+
private taken;
|
|
30
|
+
private elapsed;
|
|
31
|
+
private pendingSeconds;
|
|
32
|
+
private gl;
|
|
33
|
+
private pbo;
|
|
34
|
+
private fence;
|
|
35
|
+
constructor(onSample: (samples: Float32Array, seconds: number) => void,
|
|
36
|
+
/** Frames between readbacks once warmed up. Reading the GPU back stalls the pipeline, so this is
|
|
37
|
+
* as rare as exposure adaptation tolerates. */
|
|
38
|
+
sampleInterval?: number);
|
|
39
|
+
render(renderer: THREE.WebGLRenderer, writeBuffer: THREE.WebGLRenderTarget, readBuffer: THREE.WebGLRenderTarget, deltaTime: number): void;
|
|
40
|
+
/**
|
|
41
|
+
* Queues a readback through a pixel-pack buffer and a fence, so the CPU never waits on the GPU: the
|
|
42
|
+
* samples arrive a frame or two later through {@link collectPendingSample}. A renderer without WebGL2
|
|
43
|
+
* buffers falls back to the synchronous read, which costs one stall per sample.
|
|
44
|
+
*/
|
|
45
|
+
private queueReadback;
|
|
46
|
+
private collectPendingSample;
|
|
47
|
+
private emit;
|
|
48
|
+
dispose(): void;
|
|
49
|
+
}
|