@jgengine/shell 0.18.0 → 0.19.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 +265 -2
- package/dist/GamePlayerShell.js +27 -4
- package/dist/JoinGate.d.ts +17 -0
- package/dist/JoinGate.js +14 -0
- package/dist/Shell3dPresentation.d.ts +9 -8
- package/dist/Shell3dPresentation.js +57 -8
- package/dist/ShellChrome.d.ts +2 -0
- package/dist/ShellChrome.js +6 -0
- package/dist/ShellHudPresentation.d.ts +9 -1
- package/dist/ShellHudPresentation.js +21 -0
- package/dist/TerritoryOverlay.d.ts +21 -0
- package/dist/TerritoryOverlay.js +9 -0
- package/dist/audio/AudioComponents.js +13 -2
- package/dist/audio/audioEngine.d.ts +15 -1
- package/dist/audio/audioEngine.js +132 -11
- package/dist/audio/audioWire.d.ts +3 -0
- package/dist/audio/audioWire.js +7 -1
- package/dist/audio/loopParams.d.ts +39 -0
- package/dist/audio/loopParams.js +43 -0
- package/dist/camera/GameCameraRig.js +1 -1
- package/dist/camera/GameFirstPersonCamera.d.ts +4 -2
- package/dist/camera/GameFirstPersonCamera.js +52 -15
- package/dist/camera/Viewports.d.ts +34 -0
- package/dist/camera/Viewports.js +123 -0
- package/dist/camera/cameraRigs.js +77 -62
- package/dist/camera/orbitCameraMath.js +2 -2
- package/dist/camera/rigMath.d.ts +82 -4
- package/dist/camera/rigMath.js +133 -15
- package/dist/defineGame.d.ts +9 -0
- package/dist/defineGame.js +38 -6
- package/dist/devtools/DevtoolsOverlay.js +2 -0
- package/dist/devtools/KeysPanel.js +3 -3
- package/dist/devtools/agentBridge.d.ts +6 -0
- package/dist/devtools/agentBridge.js +15 -1
- package/dist/diagnostics/FpsCounter.d.ts +17 -0
- package/dist/diagnostics/FpsCounter.js +32 -0
- package/dist/drivers/FrameDriver.d.ts +5 -1
- package/dist/drivers/FrameDriver.js +74 -26
- package/dist/drivers/FrameRateLimiter.d.ts +9 -0
- package/dist/drivers/FrameRateLimiter.js +39 -0
- package/dist/drivers/HudOnlyDriver.js +10 -5
- package/dist/environment/DayNightSky.js +5 -5
- package/dist/environment/Daylight.d.ts +35 -1
- package/dist/environment/Daylight.js +98 -28
- package/dist/environment/EnvironmentScene.js +1 -1
- package/dist/environment/VolumetricClouds.js +20 -2
- package/dist/environment/daylightCycle.d.ts +20 -0
- package/dist/environment/daylightCycle.js +59 -7
- package/dist/environment/sunShadowMath.d.ts +45 -0
- package/dist/environment/sunShadowMath.js +50 -0
- package/dist/input/gamepadMerge.d.ts +10 -0
- package/dist/input/gamepadMerge.js +12 -0
- package/dist/input/gamepadPoll.d.ts +47 -0
- package/dist/input/gamepadPoll.js +142 -0
- package/dist/input/gamepadSource.d.ts +22 -0
- package/dist/input/gamepadSource.js +153 -0
- package/dist/input/mouseLook.js +2 -1
- package/dist/input/padHaptics.d.ts +17 -0
- package/dist/input/padHaptics.js +32 -0
- package/dist/input/pointerLock.d.ts +11 -0
- package/dist/input/pointerLock.js +19 -0
- package/dist/materialOverride.d.ts +4 -1
- package/dist/materialOverride.js +10 -1
- package/dist/postfx/PostProcessing.d.ts +11 -8
- package/dist/postfx/PostProcessing.js +38 -16
- package/dist/postfx/gradeShader.js +13 -0
- package/dist/postfx/stylizePass.d.ts +23 -0
- package/dist/postfx/stylizePass.js +216 -0
- package/dist/render/CascadedShadows.d.ts +11 -0
- package/dist/render/CascadedShadows.js +19 -8
- package/dist/render/EnvironmentLighting.d.ts +4 -1
- package/dist/render/EnvironmentLighting.js +45 -9
- package/dist/render/SceneLighting.d.ts +16 -2
- package/dist/render/SceneLighting.js +56 -6
- package/dist/render/SceneModels.js +45 -9
- package/dist/render/SkinnedInstances.d.ts +91 -0
- package/dist/render/SkinnedInstances.js +249 -0
- package/dist/render/TextureFiltering.d.ts +16 -0
- package/dist/render/TextureFiltering.js +64 -0
- package/dist/render/displayColor.d.ts +33 -0
- package/dist/render/displayColor.js +138 -0
- package/dist/render/modelLoad.d.ts +39 -0
- package/dist/render/modelLoad.js +89 -1
- package/dist/render/modelRender.d.ts +3 -0
- package/dist/render/modelRender.js +6 -2
- package/dist/render/resolveModel.js +4 -0
- package/dist/render/useFootIk.d.ts +58 -0
- package/dist/render/useFootIk.js +216 -0
- package/dist/render/useModelAnimation.d.ts +26 -0
- package/dist/render/useModelAnimation.js +184 -107
- package/dist/scene/AuthoredScene.d.ts +9 -0
- package/dist/scene/AuthoredScene.js +17 -1
- package/dist/scene/CityRenderer.js +666 -241
- package/dist/scene/cityWindowMaterial.d.ts +8 -6
- package/dist/scene/cityWindowMaterial.js +80 -21
- package/dist/settings/QuickControls.js +1 -1
- package/dist/settings/SettingsMenu.js +1 -1
- package/dist/settings/appliedSettings.d.ts +10 -2
- package/dist/settings/appliedSettings.js +9 -5
- package/dist/settings/settingsController.d.ts +3 -0
- package/dist/settings/settingsController.js +47 -3
- package/dist/structures/BuildingKitBatch.js +26 -22
- package/dist/structures/GeneratedBuilding.d.ts +3 -15
- package/dist/structures/GeneratedBuilding.js +21 -15
- package/dist/structures/buildingSurface.d.ts +21 -0
- package/dist/structures/buildingSurface.js +134 -0
- package/dist/terrain/terrainDetailMaterial.js +7 -3
- package/dist/useShellMultiplayerSync.d.ts +9 -2
- package/dist/useShellMultiplayerSync.js +30 -8
- package/dist/visibility/CullingProvider.d.ts +2 -1
- package/dist/visibility/CullingProvider.js +7 -2
- package/dist/world/GridWorldScene.js +4 -0
- package/dist/world/TileLayerRenderer.d.ts +9 -0
- package/dist/world/TileLayerRenderer.js +64 -0
- package/dist/world/WorldParticles.d.ts +31 -0
- package/dist/world/WorldParticles.js +128 -0
- package/dist/world/WorldScene.d.ts +2 -1
- package/dist/world/WorldScene.js +46 -4
- package/dist/world/entityPose.d.ts +4 -0
- package/dist/world/entityPose.js +9 -0
- package/dist/world/tileLayerInstances.d.ts +11 -0
- package/dist/world/tileLayerInstances.js +28 -0
- package/dist/worldSync.d.ts +5 -1
- package/dist/worldSync.js +25 -4
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +2 -2
- package/skills/game-design/SKILL.md +11 -1
- package/skills/jgengine/SKILL.md +9 -5
- package/skills/jgengine/api.md +205 -101
- package/skills/jgengine/capabilities.md +56 -0
- package/skills/jgengine/recipes/minimal-game.md +3 -0
- package/skills/jgengine-ui/SKILL.md +20 -15
- package/skills/jgengine-ui/api.md +538 -326
- package/skills/jgengine-ui/capabilities.md +37 -0
- package/skills/jgengine-ui/reference-primitives.md +89 -0
- package/skills/jgengine-ui/reference.md +2 -78
- package/skills/jgengine-world/SKILL.md +21 -3
- package/skills/jgengine-world/api.md +1172 -625
- package/skills/jgengine-world/capabilities.md +229 -3
- package/skills/jgengine-world/recipes/character-animation.md +39 -0
- package/skills/jgengine-world/recipes/controls.md +44 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +43 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
- package/skills/jgengine-world/reference.md +31 -2
- package/skills/jgengine-world/visibility.md +112 -0
- package/skills/level-design/SKILL.md +1 -1
- package/skills/jgengine-ui/references/visual-scorecard.md +0 -73
|
@@ -6,6 +6,8 @@ import { pointerAimFor } from "../shellPointer.js";
|
|
|
6
6
|
import { shellDrivesPlayerPose } from "../shellMovement.js";
|
|
7
7
|
import { steerYaw } from "@jgengine/core/movement/steering";
|
|
8
8
|
import { stepPlayerMovement, resolvePlayerMovementTuning } from "@jgengine/core/movement/playerMovement";
|
|
9
|
+
import { localPlayers } from "@jgengine/core/runtime/localPlayers";
|
|
10
|
+
import { seatCameraYaw } from "../camera/Viewports.js";
|
|
9
11
|
import { isServerAuthoritative } from "@jgengine/core/runtime/adapter";
|
|
10
12
|
import { resolveCommandSink } from "../commandSink.js";
|
|
11
13
|
import { inputFramesEqual, resolveInputSink } from "../inputSink.js";
|
|
@@ -17,9 +19,10 @@ import { devtools } from "@jgengine/core/devtools/devtools";
|
|
|
17
19
|
import { collisionDebug } from "../devtools/collisionDebug.js";
|
|
18
20
|
import { GAME_SIM_FRAME_PRIORITY } from "../camera/index.js";
|
|
19
21
|
import { NO_ACTIONS } from "../shellConstants.js";
|
|
22
|
+
import { createPredictionBuffer } from "@jgengine/core/runtime/prediction";
|
|
20
23
|
const TURN_SPEED = 2.4;
|
|
21
24
|
export const POSTER_SETTLE_SECONDS = 1.6;
|
|
22
|
-
export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, pointerAxisRef, analogRef, gateRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, poster, onPosterSettled, }) {
|
|
25
|
+
export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, pointerAxisRef, analogRef, gateRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, poster, onPosterSettled, authoritativeFrameRef, }) {
|
|
23
26
|
const posterElapsedRef = useRef(0);
|
|
24
27
|
const posterDoneRef = useRef(false);
|
|
25
28
|
const hasReportedTickError = useRef(false);
|
|
@@ -56,7 +59,26 @@ export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryC
|
|
|
56
59
|
}).send(frame),
|
|
57
60
|
}), [multiplayer, serverAuthoritative, serverIdRef]);
|
|
58
61
|
const lastSentInputRef = useRef(null);
|
|
59
|
-
const
|
|
62
|
+
const predictionRef = useRef(createPredictionBuffer({
|
|
63
|
+
initial: [0, 0, 0],
|
|
64
|
+
maxTicks: 120,
|
|
65
|
+
step: (state) => state,
|
|
66
|
+
snapThreshold: 0.1,
|
|
67
|
+
}));
|
|
68
|
+
const predictionHandler = useMemo(() => (_revision) => {
|
|
69
|
+
const playerId = ctx.player.possession.active(ctx.player.userId);
|
|
70
|
+
const player = ctx.scene.entity.get(playerId);
|
|
71
|
+
if (player === null)
|
|
72
|
+
return;
|
|
73
|
+
const prediction = predictionRef.current;
|
|
74
|
+
const current = player.position;
|
|
75
|
+
const snapshot = prediction.snapshot();
|
|
76
|
+
const tick = snapshot.records.at(-1)?.tick ?? snapshot.confirmedTick;
|
|
77
|
+
const result = prediction.reconcile(tick, current);
|
|
78
|
+
if (result.snapped)
|
|
79
|
+
ctx.scene.entity.setPose(playerId, { position: current });
|
|
80
|
+
}, [ctx]);
|
|
81
|
+
authoritativeFrameRef.current = serverAuthoritative ? predictionHandler : null;
|
|
60
82
|
useFrame((_state, rawDt) => {
|
|
61
83
|
if (poster) {
|
|
62
84
|
if (posterDoneRef.current)
|
|
@@ -71,7 +93,12 @@ export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryC
|
|
|
71
93
|
const sendInput = () => {
|
|
72
94
|
if (!serverAuthoritative)
|
|
73
95
|
return;
|
|
74
|
-
const frame = {
|
|
96
|
+
const frame = {
|
|
97
|
+
held: ctx.input.held(),
|
|
98
|
+
pointer: ctx.input.pointer(),
|
|
99
|
+
analog: ctx.input.analog(),
|
|
100
|
+
tick: ctx.sim.tick(),
|
|
101
|
+
};
|
|
75
102
|
const last = lastSentInputRef.current;
|
|
76
103
|
if (last !== null && inputFramesEqual(last, frame))
|
|
77
104
|
return;
|
|
@@ -88,8 +115,7 @@ export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryC
|
|
|
88
115
|
try {
|
|
89
116
|
let endPhase = devtools.profile.begin("time+input");
|
|
90
117
|
const dt = Math.min(rawDt, 0.05);
|
|
91
|
-
|
|
92
|
-
ctx.input.publish(heldActionsFor(tracker, inputActions));
|
|
118
|
+
ctx.input.publish(heldActionsFor(tracker, tracker.actions()));
|
|
93
119
|
ctx.input.publishPointer(pointerAxisRef.current);
|
|
94
120
|
ctx.input.publishAnalog(analogRef.current);
|
|
95
121
|
sendInput();
|
|
@@ -98,28 +124,50 @@ export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryC
|
|
|
98
124
|
yawRef.current = steerYaw(yawRef.current, turnInput, TURN_SPEED, dt);
|
|
99
125
|
endPhase();
|
|
100
126
|
const playerId = ctx.player.possession.active(ctx.player.userId);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
127
|
+
ctx.sim.advance(dt, (stepDt) => {
|
|
128
|
+
const gameDt = ctx.time.advance(stepDt);
|
|
129
|
+
ctx.sim.runStages("beforeMovement", stepDt);
|
|
130
|
+
const player = ctx.scene.entity.get(playerId);
|
|
131
|
+
// Server-authoritative sessions still run the deterministic local step as a prediction; the
|
|
132
|
+
// replicated pose is the confirmation that the transport reconciles on the next world diff.
|
|
133
|
+
if (player !== null && drivesPose) {
|
|
134
|
+
const endPose = devtools.profile.begin("pose");
|
|
135
|
+
stepPlayerMovement(ctx, ctx.player.userId, { held: ctx.input.held(), pointer: ctx.input.pointer(), analog: ctx.input.analog() }, stepDt, movementTuning, yawRef.current, pitchRef.current);
|
|
136
|
+
const predicted = ctx.scene.entity.get(playerId);
|
|
137
|
+
if (predicted !== null) {
|
|
138
|
+
predictionRef.current.record({ held: ctx.input.held(), pointer: ctx.input.pointer(), analog: ctx.input.analog(), tick: ctx.sim.tick() }, stepDt, predicted.position);
|
|
139
|
+
}
|
|
140
|
+
endPose();
|
|
114
141
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
142
|
+
if (drivesPose && !serverAuthoritative) {
|
|
143
|
+
const table = localPlayers(ctx);
|
|
144
|
+
const seats = table.slots();
|
|
145
|
+
for (let index = 1; index < seats.length; index += 1) {
|
|
146
|
+
const seat = table.local(seats[index].slotId);
|
|
147
|
+
if (seat === null)
|
|
148
|
+
continue;
|
|
149
|
+
stepPlayerMovement(ctx, seat.userId, { held: seat.input.held(), pointer: null, analog: seat.input.analog() }, stepDt, movementTuning, seatCameraYaw(ctx, seat.userId));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
ctx.sim.runStages("afterMovement", stepDt);
|
|
153
|
+
if (autoPickupRadius !== null && !serverAuthoritative) {
|
|
154
|
+
const endPickup = devtools.profile.begin("pickup");
|
|
155
|
+
const self = ctx.scene.entity.get(playerId);
|
|
156
|
+
if (self !== null) {
|
|
157
|
+
const nearest = ctx.scene.worldItem.nearestInRadius(self.position, autoPickupRadius);
|
|
158
|
+
if (nearest !== null)
|
|
159
|
+
ctx.scene.worldItem.pickup(nearest, ctx.player.userId);
|
|
160
|
+
}
|
|
161
|
+
endPickup();
|
|
162
|
+
}
|
|
163
|
+
if (!serverAuthoritative) {
|
|
164
|
+
devtools.profile.measure("onTick", () => {
|
|
165
|
+
playable.loop.onTick(ctx, gameDt);
|
|
166
|
+
});
|
|
167
|
+
advanceBehaviors(ctx, gameDt);
|
|
168
|
+
}
|
|
169
|
+
ctx.sim.runStages("afterTick", stepDt);
|
|
170
|
+
});
|
|
123
171
|
endPhase = devtools.profile.begin("actions");
|
|
124
172
|
if (tracker.wasPressed("tabTarget")) {
|
|
125
173
|
if (ctx.game.commands.has("target.cycle"))
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drives the Canvas at a capped frame rate. Mount only with `frameloop="never"`: it runs its
|
|
3
|
+
* own requestAnimationFrame loop and advances R3F on the refreshes `paceFrame` admits, so
|
|
4
|
+
* simulation, useFrame and rendering all run at the cap with correct deltas.
|
|
5
|
+
* @internal shell-internal; driven by the player's frame-rate limit setting.
|
|
6
|
+
*/
|
|
7
|
+
export declare function FrameRateLimiter({ fps }: {
|
|
8
|
+
fps: number;
|
|
9
|
+
}): null;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useThree } from "@react-three/fiber";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { paceFrame } from "@jgengine/core/settings/frameRateLimit";
|
|
4
|
+
/**
|
|
5
|
+
* Drives the Canvas at a capped frame rate. Mount only with `frameloop="never"`: it runs its
|
|
6
|
+
* own requestAnimationFrame loop and advances R3F on the refreshes `paceFrame` admits, so
|
|
7
|
+
* simulation, useFrame and rendering all run at the cap with correct deltas.
|
|
8
|
+
* @internal shell-internal; driven by the player's frame-rate limit setting.
|
|
9
|
+
*/
|
|
10
|
+
export function FrameRateLimiter({ fps }) {
|
|
11
|
+
const advance = useThree((state) => state.advance);
|
|
12
|
+
const get = useThree((state) => state.get);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (fps <= 0)
|
|
15
|
+
return;
|
|
16
|
+
// R3F's "never" mode reads the timestamp as seconds on its own clock; continue that clock.
|
|
17
|
+
const base = get().clock.elapsedTime;
|
|
18
|
+
let start = null;
|
|
19
|
+
let anchor = 0;
|
|
20
|
+
let frame = requestAnimationFrame(function tick(now) {
|
|
21
|
+
frame = requestAnimationFrame(tick);
|
|
22
|
+
if (start === null) {
|
|
23
|
+
start = now;
|
|
24
|
+
anchor = now;
|
|
25
|
+
}
|
|
26
|
+
const next = paceFrame(now, anchor, fps);
|
|
27
|
+
if (next === null)
|
|
28
|
+
return;
|
|
29
|
+
anchor = next;
|
|
30
|
+
advance(base + (now - start) / 1000, true);
|
|
31
|
+
});
|
|
32
|
+
return () => {
|
|
33
|
+
cancelAnimationFrame(frame);
|
|
34
|
+
// Hand the clock back to R3F's own loop in milliseconds so its next delta is one frame.
|
|
35
|
+
get().clock.oldTime = performance.now();
|
|
36
|
+
};
|
|
37
|
+
}, [fps, advance, get]);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
@@ -26,14 +26,19 @@ export function HudOnlyDriver({ ctx, playable, tracker, pointerAxisRef, gateRef,
|
|
|
26
26
|
try {
|
|
27
27
|
let endPhase = devtools.profile.begin("time+input");
|
|
28
28
|
const dt = Math.min(rawDt, 0.05);
|
|
29
|
-
|
|
30
|
-
ctx.input.publish(heldActionsFor(tracker, inputActions));
|
|
29
|
+
ctx.input.publish(heldActionsFor(tracker, tracker.actions()));
|
|
31
30
|
ctx.input.publishPointer(pointerAxisRef.current);
|
|
32
31
|
endPhase();
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
ctx.sim.advance(dt, (stepDt) => {
|
|
33
|
+
const gameDt = ctx.time.advance(stepDt);
|
|
34
|
+
ctx.sim.runStages("beforeMovement", stepDt);
|
|
35
|
+
ctx.sim.runStages("afterMovement", stepDt);
|
|
36
|
+
devtools.profile.measure("onTick", () => {
|
|
37
|
+
playable.loop.onTick(ctx, gameDt);
|
|
38
|
+
});
|
|
39
|
+
advanceBehaviors(ctx, gameDt);
|
|
40
|
+
ctx.sim.runStages("afterTick", stepDt);
|
|
35
41
|
});
|
|
36
|
-
advanceBehaviors(ctx, gameDt);
|
|
37
42
|
endPhase = devtools.profile.begin("actions");
|
|
38
43
|
const nowMs = performance.now();
|
|
39
44
|
for (const action of inputActions) {
|
|
@@ -2,7 +2,7 @@ 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 { useMemo, useRef } from "react";
|
|
4
4
|
import * as THREE from "three";
|
|
5
|
-
import { SkyDome } from "./Daylight.js";
|
|
5
|
+
import { setSkyDomeColors, SkyDome, SunLight } from "./Daylight.js";
|
|
6
6
|
import { daylightStateAt } from "./daylightCycle.js";
|
|
7
7
|
const HEMI_GROUND = "#4c6b34";
|
|
8
8
|
/**
|
|
@@ -22,22 +22,22 @@ export function DayNightSky({ cycle, radius, hazeStrength, sunGlowStrength, ligh
|
|
|
22
22
|
const hemiRef = useRef(null);
|
|
23
23
|
const initial = useMemo(() => cycle.sample(), [cycle]);
|
|
24
24
|
const initialSun = useMemo(() => daylightStateAt(initial.dayFraction).sunPosition, [initial.dayFraction]);
|
|
25
|
+
const sunDirectionRef = useRef(initialSun);
|
|
25
26
|
useFrame(() => {
|
|
26
27
|
const sample = cycle.sample();
|
|
27
28
|
const geometry = daylightStateAt(sample.dayFraction);
|
|
28
29
|
const material = skyMaterialRef.current;
|
|
29
30
|
if (material !== null) {
|
|
30
|
-
material.
|
|
31
|
-
material.uniforms.bottomColor.value.set(sample.color);
|
|
31
|
+
setSkyDomeColors(material, sample.lightColor, sample.color);
|
|
32
32
|
material.uniforms.uSunColor.value.set(sample.lightColor);
|
|
33
33
|
material.uniforms.uSunDirection.value
|
|
34
34
|
.set(geometry.sunPosition[0], geometry.sunPosition[1], geometry.sunPosition[2])
|
|
35
35
|
.normalize();
|
|
36
36
|
material.uniforms.uSunIntensity.value = sample.intensity;
|
|
37
37
|
}
|
|
38
|
+
sunDirectionRef.current = geometry.sunPosition;
|
|
38
39
|
const sun = sunRef.current;
|
|
39
40
|
if (sun !== null) {
|
|
40
|
-
sun.position.set(geometry.sunPosition[0], geometry.sunPosition[1], geometry.sunPosition[2]);
|
|
41
41
|
sun.intensity = sample.intensity * keyLightIntensity;
|
|
42
42
|
sun.color.set(sample.lightColor);
|
|
43
43
|
}
|
|
@@ -47,5 +47,5 @@ export function DayNightSky({ cycle, radius, hazeStrength, sunGlowStrength, ligh
|
|
|
47
47
|
hemi.color.set(sample.lightColor);
|
|
48
48
|
}
|
|
49
49
|
});
|
|
50
|
-
return (_jsxs(_Fragment, { children: [_jsx(SkyDome, { topColor: initial.lightColor, horizonColor: initial.color, sunDirection: initialSun, sunColor: initial.lightColor, sunIntensity: initial.intensity, materialRef: skyMaterialRef, ...(radius === undefined ? {} : { radius }), ...(hazeStrength === undefined ? {} : { hazeStrength }), ...(sunGlowStrength === undefined ? {} : { sunGlowStrength }) }), lights ? (_jsxs(_Fragment, { children: [_jsx("hemisphereLight", { ref: hemiRef, args: [initial.lightColor, HEMI_GROUND, initial.intensity * ambientIntensity] }), _jsx(
|
|
50
|
+
return (_jsxs(_Fragment, { children: [_jsx(SkyDome, { topColor: initial.lightColor, horizonColor: initial.color, sunDirection: initialSun, sunColor: initial.lightColor, sunIntensity: initial.intensity, materialRef: skyMaterialRef, ...(radius === undefined ? {} : { radius }), ...(hazeStrength === undefined ? {} : { hazeStrength }), ...(sunGlowStrength === undefined ? {} : { sunGlowStrength }) }), lights ? (_jsxs(_Fragment, { children: [_jsx("hemisphereLight", { ref: hemiRef, args: [initial.lightColor, HEMI_GROUND, initial.intensity * ambientIntensity] }), _jsx(SunLight, { direction: initialSun, directionRef: sunDirectionRef, lightRef: sunRef, intensity: initial.intensity * keyLightIntensity, color: initial.lightColor })] })) : null] }));
|
|
51
51
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type MutableRefObject } from "react";
|
|
1
|
+
import { type MutableRefObject, type RefObject } from "react";
|
|
2
2
|
import * as THREE from "three";
|
|
3
3
|
import type { BiomeBand, SkyEnvironmentDescriptor } from "@jgengine/core/world/features";
|
|
4
4
|
import { type VolumetricCloudsConfig } from "@jgengine/core/world/volumetricClouds";
|
|
@@ -23,6 +23,40 @@ export interface SkyDomeProps {
|
|
|
23
23
|
/** Exposes the created shader material so a time-of-day driver can mutate its uniforms per frame without recreating it. */
|
|
24
24
|
materialRef?: MutableRefObject<THREE.ShaderMaterial | null>;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Shadow-casting sun whose shadow box leads the camera each frame ({@link sunShadowFocus}), so
|
|
28
|
+
* grounded shadows stay crisp and soft-edged under the player anywhere in a large world. Time-of-day
|
|
29
|
+
* drivers aim it through `directionRef` and retint it through `lightRef`.
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
export declare function SunLight({ direction, directionRef, lightRef, intensity, color, }: {
|
|
33
|
+
direction: readonly [number, number, number];
|
|
34
|
+
directionRef?: MutableRefObject<readonly [number, number, number]>;
|
|
35
|
+
lightRef?: RefObject<THREE.DirectionalLight | null>;
|
|
36
|
+
intensity: number;
|
|
37
|
+
color: string;
|
|
38
|
+
}): import("react").JSX.Element;
|
|
39
|
+
/**
|
|
40
|
+
* Sets the zenith and horizon colors a sky dome should show on screen. The dome converts them each
|
|
41
|
+
* frame through the renderer's tone mapping ({@link DisplayColorWriter}), so a picked swatch renders
|
|
42
|
+
* as picked instead of greyed by the output curve.
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
export declare function setSkyDomeColors(material: THREE.ShaderMaterial, top: THREE.ColorRepresentation, bottom: THREE.ColorRepresentation): void;
|
|
46
|
+
/**
|
|
47
|
+
* Scene fog whose color is a display color, converted through the renderer's tone mapping like the
|
|
48
|
+
* dome's horizon so fogged ground meets the sky without a seam. Drivers retint it with
|
|
49
|
+
* {@link setSkyFogColor}.
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
export declare function SkyFog({ color, near, far, fogRef }: {
|
|
53
|
+
color: string;
|
|
54
|
+
near: number;
|
|
55
|
+
far: number;
|
|
56
|
+
fogRef?: RefObject<THREE.Fog | null>;
|
|
57
|
+
}): import("react").JSX.Element;
|
|
58
|
+
/** Sets the display color a {@link SkyFog} converts each frame. @internal */
|
|
59
|
+
export declare function setSkyFogColor(fog: THREE.Fog, color: THREE.ColorRepresentation): void;
|
|
26
60
|
/** @internal */
|
|
27
61
|
export declare function SkyDome({ topColor, horizonColor, radius, offset, exponent, sunDirection, sunColor, sunIntensity, hazeStrength, sunGlowStrength, cloudiness, materialRef, }?: SkyDomeProps): import("react").JSX.Element;
|
|
28
62
|
export interface DaylightProps {
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useFrame } from "@react-three/fiber";
|
|
3
|
-
import { useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
3
|
+
import { useEffect, useLayoutEffect, useMemo, useRef } 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";
|
|
7
|
+
import { warnOnce } from "@jgengine/core/devtools/warnOnce";
|
|
8
|
+
import { DisplayColorWriter } from "../render/displayColor.js";
|
|
7
9
|
import { daylightStateAt, SKY_PRESET_DAY_FRACTION } from "./daylightCycle.js";
|
|
10
|
+
import { SUN_SHADOW, sunLightPosition, sunShadowFocus } from "./sunShadowMath.js";
|
|
8
11
|
import { VolumetricClouds } from "./VolumetricClouds.js";
|
|
9
12
|
function normalizeVec3(v) {
|
|
10
13
|
const out = new THREE.Vector3(v[0], v[1], v[2]);
|
|
@@ -17,29 +20,77 @@ const SUN_COLOR = "#fff1c9";
|
|
|
17
20
|
const HEMI_SKY = "#bfe3ff";
|
|
18
21
|
const HEMI_GROUND = "#4c6b34";
|
|
19
22
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
+
* Shadow-casting sun whose shadow box leads the camera each frame ({@link sunShadowFocus}), so
|
|
24
|
+
* grounded shadows stay crisp and soft-edged under the player anywhere in a large world. Time-of-day
|
|
25
|
+
* drivers aim it through `directionRef` and retint it through `lightRef`.
|
|
26
|
+
* @internal
|
|
23
27
|
*/
|
|
24
|
-
function
|
|
25
|
-
const
|
|
28
|
+
export function SunLight({ direction, directionRef, lightRef, intensity, color, }) {
|
|
29
|
+
const ownRef = useRef(null);
|
|
30
|
+
const ref = lightRef ?? ownRef;
|
|
31
|
+
const scratch = useMemo(() => ({ forward: new THREE.Vector3(), focus: { x: 0, z: 0 }, position: [0, 0, 0] }), []);
|
|
26
32
|
useFrame((state) => {
|
|
27
33
|
const light = ref.current;
|
|
28
34
|
if (light === null)
|
|
29
35
|
return;
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
light.
|
|
36
|
+
state.camera.getWorldDirection(scratch.forward);
|
|
37
|
+
const focus = sunShadowFocus(state.camera.position, scratch.forward, scratch.focus);
|
|
38
|
+
const [x, y, z] = sunLightPosition(focus, directionRef?.current ?? direction, scratch.position);
|
|
39
|
+
light.position.set(x, y, z);
|
|
40
|
+
light.target.position.set(focus.x, 0, focus.z);
|
|
34
41
|
light.target.updateMatrixWorld();
|
|
35
42
|
});
|
|
36
|
-
|
|
43
|
+
const initial = sunLightPosition({ x: 0, z: 0 }, direction);
|
|
44
|
+
return (_jsx("directionalLight", { ref: ref, position: initial, intensity: intensity, color: color, castShadow: true, "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 }));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Sets the zenith and horizon colors a sky dome should show on screen. The dome converts them each
|
|
48
|
+
* frame through the renderer's tone mapping ({@link DisplayColorWriter}), so a picked swatch renders
|
|
49
|
+
* as picked instead of greyed by the output curve.
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
export function setSkyDomeColors(material, top, bottom) {
|
|
53
|
+
const display = material.userData;
|
|
54
|
+
(display.top ??= new THREE.Color()).set(top);
|
|
55
|
+
(display.bottom ??= new THREE.Color()).set(bottom);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Scene fog whose color is a display color, converted through the renderer's tone mapping like the
|
|
59
|
+
* dome's horizon so fogged ground meets the sky without a seam. Drivers retint it with
|
|
60
|
+
* {@link setSkyFogColor}.
|
|
61
|
+
* @internal
|
|
62
|
+
*/
|
|
63
|
+
export function SkyFog({ color, near, far, fogRef }) {
|
|
64
|
+
const ownRef = useRef(null);
|
|
65
|
+
const ref = fogRef ?? ownRef;
|
|
66
|
+
const writer = useMemo(() => new DisplayColorWriter(), []);
|
|
67
|
+
useLayoutEffect(() => {
|
|
68
|
+
if (ref.current !== null)
|
|
69
|
+
setSkyFogColor(ref.current, color);
|
|
70
|
+
}, [ref, color]);
|
|
71
|
+
useFrame((state) => {
|
|
72
|
+
const fog = ref.current;
|
|
73
|
+
const display = fog === null ? undefined : fogDisplayColors.get(fog);
|
|
74
|
+
if (fog === null || display === undefined)
|
|
75
|
+
return;
|
|
76
|
+
writer.write(fog.color, display, state.gl);
|
|
77
|
+
});
|
|
78
|
+
return _jsx("fog", { attach: "fog", ref: ref, args: [color, near, far] });
|
|
37
79
|
}
|
|
80
|
+
/** Sets the display color a {@link SkyFog} converts each frame. @internal */
|
|
81
|
+
export function setSkyFogColor(fog, color) {
|
|
82
|
+
const display = fogDisplayColors.get(fog);
|
|
83
|
+
if (display === undefined)
|
|
84
|
+
fogDisplayColors.set(fog, new THREE.Color(color));
|
|
85
|
+
else
|
|
86
|
+
display.set(color);
|
|
87
|
+
}
|
|
88
|
+
const fogDisplayColors = new WeakMap();
|
|
38
89
|
/** @internal */
|
|
39
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, } = {}) {
|
|
40
91
|
const meshRef = useRef(null);
|
|
41
92
|
const material = useMemo(() => {
|
|
42
|
-
|
|
93
|
+
const dome = new THREE.ShaderMaterial({
|
|
43
94
|
uniforms: {
|
|
44
95
|
topColor: { value: new THREE.Color(topColor) },
|
|
45
96
|
bottomColor: { value: new THREE.Color(horizonColor) },
|
|
@@ -93,12 +144,16 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
93
144
|
float glow = pow(sd, 8.0) * 0.35 + pow(sd, 128.0) * 2.6;
|
|
94
145
|
col += uSunColor * glow * uSunIntensity * uSunGlow;
|
|
95
146
|
gl_FragColor = vec4(col, 1.0);
|
|
147
|
+
#include <tonemapping_fragment>
|
|
148
|
+
#include <colorspace_fragment>
|
|
96
149
|
}
|
|
97
150
|
`,
|
|
98
151
|
side: THREE.BackSide,
|
|
99
152
|
depthWrite: false,
|
|
100
153
|
fog: false,
|
|
101
154
|
});
|
|
155
|
+
setSkyDomeColors(dome, topColor, horizonColor);
|
|
156
|
+
return dome;
|
|
102
157
|
}, [topColor, horizonColor, offset, exponent, sunColor, sunDirection, sunIntensity, hazeStrength, sunGlowStrength, cloudiness]);
|
|
103
158
|
useEffect(() => {
|
|
104
159
|
if (materialRef !== undefined)
|
|
@@ -109,7 +164,18 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
109
164
|
materialRef.current = null;
|
|
110
165
|
};
|
|
111
166
|
}, [material, materialRef]);
|
|
167
|
+
const camera = useThree((state) => state.camera);
|
|
168
|
+
const writers = useMemo(() => ({ top: new DisplayColorWriter(), bottom: new DisplayColorWriter() }), []);
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
const far = camera.far;
|
|
171
|
+
if (typeof far !== "number" || radius < far)
|
|
172
|
+
return;
|
|
173
|
+
warnOnce("sky-dome-radius", `[jgengine] sky radius ${radius} is at or beyond the camera far plane ${far}; the dome depth-clips to black. Lower sky.radius or raise camera.frustum.far.`);
|
|
174
|
+
}, [camera, radius]);
|
|
112
175
|
useFrame((state) => {
|
|
176
|
+
const display = material.userData;
|
|
177
|
+
writers.top.write(material.uniforms.topColor.value, display.top, state.gl);
|
|
178
|
+
writers.bottom.write(material.uniforms.bottomColor.value, display.bottom, state.gl);
|
|
113
179
|
const mesh = meshRef.current;
|
|
114
180
|
if (mesh === null)
|
|
115
181
|
return;
|
|
@@ -118,10 +184,14 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
118
184
|
});
|
|
119
185
|
return (_jsx("mesh", { ref: meshRef, material: material, renderOrder: -1, frustumCulled: false, children: _jsx("sphereGeometry", { args: [radius, 32, 16] }) }));
|
|
120
186
|
}
|
|
187
|
+
/** Points the dome's sun glow at `position` and keeps the disc lit; called per frame by the time-of-day drivers. */
|
|
188
|
+
function aimSkySun(material, position) {
|
|
189
|
+
material.uniforms.uSunDirection.value.set(position[0], position[1], position[2]).normalize();
|
|
190
|
+
}
|
|
121
191
|
/** @internal */
|
|
122
192
|
export function Daylight({ sky, fog, sun, ambient, lights = true, clouds } = {}) {
|
|
123
193
|
const sunPosition = sun?.position ?? [120, 160, 70];
|
|
124
|
-
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(
|
|
194
|
+
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(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 })] })) : null] }));
|
|
125
195
|
}
|
|
126
196
|
/** True when at least one band overrides fog or sky — the trigger for the per-frame biome driver. */
|
|
127
197
|
function bandsDriveSkyOrFog(bands) {
|
|
@@ -172,6 +242,7 @@ function BiomeDaylight({ sky, bands, clock, lights, }) {
|
|
|
172
242
|
const baseFraction = timeOfDay ? clock.calendar().dayFraction : SKY_PRESET_DAY_FRACTION[sky.preset];
|
|
173
243
|
const initial = useMemo(() => daylightStateAt(baseFraction, sky), [sky, baseFraction]);
|
|
174
244
|
const sunRef = useRef(null);
|
|
245
|
+
const sunDirectionRef = useRef(initial.sunPosition);
|
|
175
246
|
const hemiRef = useRef(null);
|
|
176
247
|
const fogRef = useRef(null);
|
|
177
248
|
const skyMaterialRef = useRef(null);
|
|
@@ -196,50 +267,49 @@ function BiomeDaylight({ sky, bands, clock, lights, }) {
|
|
|
196
267
|
const skyValue = skySampler(z);
|
|
197
268
|
const fogNode = fogRef.current;
|
|
198
269
|
if (fogNode !== null) {
|
|
199
|
-
fogNode
|
|
270
|
+
setSkyFogColor(fogNode, fog.color);
|
|
200
271
|
fogNode.near = fog.near;
|
|
201
272
|
fogNode.far = fog.far;
|
|
202
273
|
}
|
|
203
274
|
const skyMaterial = skyMaterialRef.current;
|
|
204
275
|
if (skyMaterial !== null) {
|
|
205
|
-
skyMaterial.
|
|
206
|
-
skyMaterial.
|
|
276
|
+
setSkyDomeColors(skyMaterial, skyValue.zenithColor, skyValue.horizonColor);
|
|
277
|
+
aimSkySun(skyMaterial, base.sunPosition);
|
|
207
278
|
}
|
|
279
|
+
sunDirectionRef.current = base.sunPosition;
|
|
208
280
|
const sun = sunRef.current;
|
|
209
|
-
if (sun !== null)
|
|
210
|
-
sun.position.set(base.sunPosition[0], base.sunPosition[1], base.sunPosition[2]);
|
|
281
|
+
if (sun !== null)
|
|
211
282
|
sun.intensity = skyValue.sunIntensity;
|
|
212
|
-
}
|
|
213
283
|
const hemi = hemiRef.current;
|
|
214
284
|
if (hemi !== null)
|
|
215
285
|
hemi.intensity = skyValue.ambientIntensity;
|
|
216
286
|
});
|
|
217
|
-
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(
|
|
287
|
+
return (_jsxs(_Fragment, { children: [_jsx(SkyDome, { topColor: initial.skyTop, horizonColor: initial.skyBottom, sunDirection: initial.sunPosition, sunColor: SUN_COLOR, materialRef: skyMaterialRef, ...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: SUN_COLOR })] })) : null] }));
|
|
218
288
|
}
|
|
219
289
|
function DrivenDaylight({ sky, clock, lights, }) {
|
|
220
290
|
const initial = useMemo(() => daylightStateAt(clock.calendar().dayFraction, sky), [clock, sky]);
|
|
221
291
|
const sunRef = useRef(null);
|
|
292
|
+
const sunDirectionRef = useRef(initial.sunPosition);
|
|
222
293
|
const hemiRef = useRef(null);
|
|
223
294
|
const fogRef = useRef(null);
|
|
224
295
|
const skyMaterialRef = useRef(null);
|
|
225
296
|
useFrame(() => {
|
|
226
297
|
const state = daylightStateAt(clock.calendar().dayFraction, sky);
|
|
298
|
+
sunDirectionRef.current = state.sunPosition;
|
|
227
299
|
const sun = sunRef.current;
|
|
228
|
-
if (sun !== null)
|
|
229
|
-
sun.position.set(state.sunPosition[0], state.sunPosition[1], state.sunPosition[2]);
|
|
300
|
+
if (sun !== null)
|
|
230
301
|
sun.intensity = state.sunIntensity;
|
|
231
|
-
}
|
|
232
302
|
const hemi = hemiRef.current;
|
|
233
303
|
if (hemi !== null)
|
|
234
304
|
hemi.intensity = state.ambientIntensity;
|
|
235
305
|
const fog = fogRef.current;
|
|
236
306
|
if (fog !== null)
|
|
237
|
-
fog
|
|
307
|
+
setSkyFogColor(fog, sky.fog?.color ?? state.background);
|
|
238
308
|
const skyMaterial = skyMaterialRef.current;
|
|
239
309
|
if (skyMaterial !== null) {
|
|
240
|
-
skyMaterial.
|
|
241
|
-
skyMaterial
|
|
310
|
+
setSkyDomeColors(skyMaterial, state.skyTop, state.skyBottom);
|
|
311
|
+
aimSkySun(skyMaterial, state.sunPosition);
|
|
242
312
|
}
|
|
243
313
|
});
|
|
244
|
-
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(
|
|
314
|
+
return (_jsxs(_Fragment, { children: [_jsx(SkyDome, { topColor: initial.skyTop, horizonColor: initial.skyBottom, sunDirection: initial.sunPosition, sunColor: SUN_COLOR, materialRef: skyMaterialRef, ...skyDomeShape(sky) }), sky.volumetricClouds === undefined ? null : (_jsx(VolumetricClouds, { rules: resolveVolumetricClouds(sky.volumetricClouds), sunDirection: initial.sunPosition })), _jsx(SkyFog, { fogRef: fogRef, color: sky.fog?.color ?? initial.background, near: sky.fog?.near ?? 70, far: sky.fog?.far ?? 260 }), 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: SUN_COLOR })] })) : null] }));
|
|
245
315
|
}
|
|
@@ -85,7 +85,7 @@ function Vegetation({ grass, field, groundColor, }) {
|
|
|
85
85
|
const wind = useMemo(() => ({ strength: grass.windStrength }), [grass.windStrength]);
|
|
86
86
|
const colorBase = grass.colors[0];
|
|
87
87
|
const colorTip = grass.colors[grass.colors.length - 1];
|
|
88
|
-
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 }) }));
|
|
88
|
+
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, 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 }) }));
|
|
89
89
|
}
|
|
90
90
|
/** Terrain color under a grass patch center — so blade roots blend into the ground they stand on. */
|
|
91
91
|
function vegetationGroundColor(terrain, field, grass) {
|
|
@@ -7,7 +7,8 @@ import { useDisposable } from "../render/useDisposable.js";
|
|
|
7
7
|
* Camera-centered proxy dome radius. Must sit inside the default camera far plane
|
|
8
8
|
* (`CAMERA_FRUSTUM_DEFAULTS.far` = 300) — a bigger sphere is clipped whole and renders nothing. The
|
|
9
9
|
* raymarch works in world space against the absolute cloud slab, so this size only has to cover the
|
|
10
|
-
* screen (camera is always at the dome center), never bound the clouds
|
|
10
|
+
* screen (camera is always at the dome center), never bound the clouds; each fragment writes its own
|
|
11
|
+
* depth at the first cloud hit, so the dome's radius never decides what occludes the clouds.
|
|
11
12
|
*/
|
|
12
13
|
const INSIDE_FRUSTUM_DOME_RADIUS = 240;
|
|
13
14
|
function hashSeed(text) {
|
|
@@ -43,8 +44,18 @@ uniform float uScale;
|
|
|
43
44
|
uniform float uSpeed;
|
|
44
45
|
uniform float uTime;
|
|
45
46
|
uniform float uSeedOffset;
|
|
47
|
+
uniform mat4 uProjection;
|
|
48
|
+
uniform mat4 uView;
|
|
46
49
|
varying vec3 vJgCloudWorldPos;
|
|
47
50
|
|
|
51
|
+
// Depth-buffer value of a world point, so the proxy dome tests against the scene at the cloud's
|
|
52
|
+
// own distance instead of the dome's: a tower in front of the slab entry then occludes the cloud.
|
|
53
|
+
float jgCloudDepthAt(vec3 worldPos){
|
|
54
|
+
vec4 clip = uProjection * uView * vec4(worldPos, 1.0);
|
|
55
|
+
float ndc = clip.z / max(clip.w, 1e-5);
|
|
56
|
+
return clamp(ndc * 0.5 + 0.5, 0.0, 0.999999);
|
|
57
|
+
}
|
|
58
|
+
|
|
48
59
|
float jgCloudHash13(vec3 p){
|
|
49
60
|
p = fract(p * 0.1031 + uSeedOffset * 0.0007);
|
|
50
61
|
p += dot(p, p.yzx + 33.33);
|
|
@@ -128,11 +139,13 @@ void main() {
|
|
|
128
139
|
vec3 color = vec3(0.0);
|
|
129
140
|
float sunDot = max(dot(rayDir, normalize(uSunDirection)), 0.0);
|
|
130
141
|
float silver = pow(sunDot, 5.0) * uSunScatter;
|
|
142
|
+
float tHit = -1.0;
|
|
131
143
|
|
|
132
144
|
for (int i = 0; i < STEPS; i++) {
|
|
133
145
|
vec3 pos = rayOrigin + rayDir * t;
|
|
134
146
|
float dens = jgCloudDensity(pos);
|
|
135
147
|
if (dens > 0.002) {
|
|
148
|
+
if (tHit < 0.0) tHit = t;
|
|
136
149
|
float light = jgCloudLight(pos);
|
|
137
150
|
float powder = 1.0 - exp(-dens * 3.0);
|
|
138
151
|
vec3 lit = mix(uColor, uSunColor, light) * (0.55 + 0.45 * powder) + uSunColor * silver * 0.5;
|
|
@@ -146,7 +159,8 @@ void main() {
|
|
|
146
159
|
}
|
|
147
160
|
|
|
148
161
|
float alpha = (1.0 - transmittance) * smoothstep(0.015, 0.22, rayDir.y);
|
|
149
|
-
if (alpha < 0.01) discard;
|
|
162
|
+
if (alpha < 0.01 || tHit < 0.0) discard;
|
|
163
|
+
gl_FragDepthEXT = jgCloudDepthAt(rayOrigin + rayDir * tHit);
|
|
150
164
|
gl_FragColor = vec4(color, alpha);
|
|
151
165
|
}
|
|
152
166
|
`;
|
|
@@ -178,6 +192,8 @@ export function VolumetricClouds({ rules, sunDirection }) {
|
|
|
178
192
|
uSpeed: { value: rules.speed },
|
|
179
193
|
uTime: { value: 0 },
|
|
180
194
|
uSeedOffset: { value: hashSeed(rules.seed) },
|
|
195
|
+
uProjection: { value: new THREE.Matrix4() },
|
|
196
|
+
uView: { value: new THREE.Matrix4() },
|
|
181
197
|
},
|
|
182
198
|
vertexShader: CLOUD_VERTEX_SHADER,
|
|
183
199
|
fragmentShader: CLOUD_FRAGMENT_SHADER,
|
|
@@ -196,6 +212,8 @@ export function VolumetricClouds({ rules, sunDirection }) {
|
|
|
196
212
|
}
|
|
197
213
|
const uniforms = material.uniforms;
|
|
198
214
|
uniforms.uCameraPos.value.copy(state.camera.position);
|
|
215
|
+
uniforms.uProjection.value.copy(state.camera.projectionMatrix);
|
|
216
|
+
uniforms.uView.value.copy(state.camera.matrixWorldInverse);
|
|
199
217
|
uniforms.uTime.value = timeRef.current;
|
|
200
218
|
});
|
|
201
219
|
return (_jsx("mesh", { ref: meshRef, material: material, renderOrder: -0.5, frustumCulled: false, children: _jsx("sphereGeometry", { args: [INSIDE_FRUSTUM_DOME_RADIUS, 32, 16] }) }));
|