@jgengine/shell 0.11.0 → 0.13.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 +52 -0
- package/dist/CombatPresentation.d.ts +5 -0
- package/dist/CombatPresentation.js +8 -0
- package/dist/GameHost.d.ts +27 -1
- package/dist/GameHost.js +83 -4
- package/dist/GamePlayer.d.ts +5 -0
- package/dist/GamePlayer.js +5 -0
- package/dist/GamePlayerShell.d.ts +5 -0
- package/dist/GamePlayerShell.js +63 -359
- package/dist/GameUiPreview.js +3 -0
- package/dist/Shell3dPresentation.d.ts +63 -0
- package/dist/Shell3dPresentation.js +302 -0
- package/dist/ShellChrome.d.ts +51 -0
- package/dist/ShellChrome.js +51 -0
- package/dist/ShellHudPresentation.d.ts +41 -0
- package/dist/ShellHudPresentation.js +22 -0
- package/dist/audio/audioEngine.d.ts +4 -0
- package/dist/audio/audioEngine.js +111 -26
- package/dist/audio/audioWire.d.ts +22 -4
- package/dist/audio/audioWire.js +46 -3
- package/dist/audio/loopParams.d.ts +20 -0
- package/dist/audio/loopParams.js +28 -0
- package/dist/camera/GameCameraRig.js +6 -1
- package/dist/camera/GameOrbitCamera.js +7 -1
- package/dist/camera/cameraCollision.d.ts +28 -0
- package/dist/camera/cameraCollision.js +34 -0
- package/dist/camera/cameraRigs.d.ts +3 -1
- package/dist/camera/cameraRigs.js +24 -10
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/rigMath.d.ts +27 -0
- package/dist/camera/rigMath.js +45 -0
- package/dist/defineGame.d.ts +15 -1
- package/dist/defineGame.js +26 -4
- package/dist/devtools/DevtoolsOverlay.d.ts +4 -4
- package/dist/devtools/DevtoolsOverlay.js +28 -12
- package/dist/devtools/agentBridge.js +4 -0
- package/dist/devtools/perfDiagnose.js +1 -1
- package/dist/diagnostics/RuntimeDiagnostics.d.ts +7 -0
- package/dist/diagnostics/RuntimeDiagnostics.js +23 -12
- package/dist/environment/EnvironmentScene.js +17 -3
- package/dist/environment/RoadJunctions.d.ts +12 -0
- package/dist/environment/RoadJunctions.js +69 -0
- package/dist/environment/RoadRibbons.d.ts +8 -2
- package/dist/environment/RoadRibbons.js +26 -8
- package/dist/environment/terrainGroundColor.d.ts +9 -0
- package/dist/environment/terrainGroundColor.js +22 -0
- package/dist/gameKit.d.ts +27 -0
- package/dist/gameKit.js +29 -0
- package/dist/map/MapMarkerBeacons.d.ts +3 -2
- package/dist/postfx/PostProcessing.d.ts +9 -1
- package/dist/postfx/PostProcessing.js +11 -4
- package/dist/presentationResolve.d.ts +39 -0
- package/dist/presentationResolve.js +53 -0
- package/dist/registry.d.ts +1 -0
- package/dist/render/PartMotion.d.ts +18 -0
- package/dist/render/PartMotion.js +91 -0
- package/dist/render/SceneModels.d.ts +9 -2
- package/dist/render/SceneModels.js +31 -180
- package/dist/render/assetBase.d.ts +17 -0
- package/dist/render/assetBase.js +35 -0
- package/dist/render/measureBounds.d.ts +34 -0
- package/dist/render/measureBounds.js +127 -0
- package/dist/render/modelLoad.js +4 -1
- package/dist/render/resolveModel.d.ts +15 -0
- package/dist/render/resolveModel.js +43 -2
- package/dist/render/useModelAnimation.d.ts +10 -0
- package/dist/render/useModelAnimation.js +198 -0
- package/dist/scatter/InstancedScatter.js +5 -0
- package/dist/scatter/scatterProxies.js +24 -0
- package/dist/scene/AuthoredScene.d.ts +3 -1
- package/dist/scene/AuthoredScene.js +7 -2
- package/dist/scene/CityRenderer.d.ts +2 -0
- package/dist/scene/CityRenderer.js +1031 -0
- package/dist/scene/GrassFieldRenderer.js +53 -5
- package/dist/scene/PoleLineRenderer.d.ts +2 -0
- package/dist/scene/PoleLineRenderer.js +287 -0
- package/dist/scene/SoilPatchRenderer.js +70 -7
- package/dist/scene/WaterRenderer.js +34 -10
- package/dist/scene/builtinSceneKindRenderers.js +4 -0
- package/dist/scene/cityWindowMaterial.d.ts +16 -0
- package/dist/scene/cityWindowMaterial.js +81 -0
- package/dist/scene/sceneKindRenderers.d.ts +2 -0
- package/dist/settings/appliedSettings.d.ts +2 -1
- package/dist/settings/appliedSettings.js +4 -2
- package/dist/structures/GeneratedBuilding.d.ts +4 -0
- package/dist/structures/GeneratedBuilding.js +17 -7
- package/dist/structures/TransformGizmo.d.ts +11 -1
- package/dist/structures/TransformGizmo.js +9 -5
- package/dist/terrain/GrassField.d.ts +11 -3
- package/dist/terrain/GrassField.js +16 -6
- package/dist/terrain/grassGeometry.d.ts +33 -1
- package/dist/terrain/grassGeometry.js +83 -21
- package/dist/terrain/grassMaterial.d.ts +18 -0
- package/dist/terrain/grassMaterial.js +66 -13
- package/dist/terrain/heightfieldGeometry.d.ts +34 -0
- package/dist/terrain/heightfieldGeometry.js +109 -0
- package/dist/terrain/index.d.ts +1 -0
- package/dist/terrain/index.js +1 -0
- package/dist/terrain/soilPatchMaterial.d.ts +15 -2
- package/dist/terrain/soilPatchMaterial.js +46 -8
- package/dist/terrain/terrainDetailMaterial.js +15 -2
- package/dist/water/Ocean.d.ts +7 -1
- package/dist/water/Ocean.js +16 -3
- package/dist/water/OceanConfig.d.ts +9 -0
- package/dist/water/OceanConfig.js +6 -0
- package/dist/water/OceanMaterial.d.ts +12 -0
- package/dist/water/OceanMaterial.js +9 -0
- package/dist/water/OceanShader.d.ts +2 -2
- package/dist/water/OceanShader.js +78 -10
- package/dist/world/PlaceScene.d.ts +11 -0
- package/dist/world/PlaceScene.js +27 -0
- package/dist/world/WorldHud.d.ts +3 -1
- package/dist/world/WorldHud.js +2 -2
- package/dist/world/WorldScene.js +32 -4
- package/dist/worldSky.d.ts +7 -1
- package/dist/worldSky.js +11 -1
- package/package.json +4 -4
- package/skills/game-design/SKILL.md +28 -0
- package/skills/game-design/agents/openai.yaml +4 -0
- package/skills/game-design/references/application-playbook.md +170 -0
- package/skills/game-design/references/field-guide.md +288 -0
- package/skills/jgengine/SKILL.md +8 -4
- package/skills/jgengine/api.md +182 -81
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +17 -10
- package/skills/jgengine-ui/api.md +316 -51
- package/skills/jgengine-ui/capabilities.md +135 -2
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +59 -9
- package/skills/jgengine-ui/references/visual-scorecard.md +1 -1
- package/skills/jgengine-world/SKILL.md +13 -0
- package/skills/jgengine-world/api.md +453 -21
- package/skills/jgengine-world/capabilities.md +112 -2
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +43 -4
- package/skills/level-design/SKILL.md +54 -0
- package/skills/level-design/agents/openai.yaml +4 -0
- package/skills/level-design/references/application-playbook.md +192 -0
- package/skills/level-design/references/field-guide.md +337 -0
- package/skills/level-design/references/live-editor-loop.md +179 -0
- package/dist/cartridge/runtime.d.ts +0 -15
- package/dist/cartridge/runtime.js +0 -556
- package/dist/cartridge/spec.d.ts +0 -188
- package/dist/cartridge/spec.js +0 -19
- package/dist/cartridge/validate.d.ts +0 -2
- package/dist/cartridge/validate.js +0 -78
- package/dist/cartridge.d.ts +0 -145
- package/dist/cartridge.js +0 -246
- package/skills/jgengine/reference-cartridge.md +0 -72
|
@@ -6,6 +6,11 @@ export const DEFAULT_GRASS_WIND = {
|
|
|
6
6
|
gustScale: 0.16,
|
|
7
7
|
flutter: 0.08,
|
|
8
8
|
};
|
|
9
|
+
/** Default camera-distance fade band: tufts start thinning at 35 m and are gone by 95 m. */
|
|
10
|
+
export const DEFAULT_GRASS_DISTANCE_FADE = {
|
|
11
|
+
start: 35,
|
|
12
|
+
end: 95,
|
|
13
|
+
};
|
|
9
14
|
function normalizeWindDirection(direction) {
|
|
10
15
|
const vector = new THREE.Vector2(direction[0], direction[1]);
|
|
11
16
|
return vector.lengthSq() === 0 ? new THREE.Vector2(1, 0) : vector.normalize();
|
|
@@ -23,8 +28,23 @@ export function resolveGrassWind(wind) {
|
|
|
23
28
|
};
|
|
24
29
|
}
|
|
25
30
|
/** @internal */
|
|
31
|
+
export function resolveGrassDistanceFade(fade) {
|
|
32
|
+
// start >= end disables the fade in the shader, so "off" is just an empty band.
|
|
33
|
+
if (fade === false)
|
|
34
|
+
return { start: 0, end: 0 };
|
|
35
|
+
return {
|
|
36
|
+
start: fade?.start ?? DEFAULT_GRASS_DISTANCE_FADE.start,
|
|
37
|
+
end: fade?.end ?? DEFAULT_GRASS_DISTANCE_FADE.end,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** @internal */
|
|
26
41
|
export function createGrassMaterial(options = {}) {
|
|
27
42
|
const wind = resolveGrassWind(options.wind);
|
|
43
|
+
const fade = resolveGrassDistanceFade(options.distanceFade);
|
|
44
|
+
const colorBase = new THREE.Color(options.colorBase ?? "#2d431f");
|
|
45
|
+
const colorGround = options.colorGround === undefined
|
|
46
|
+
? colorBase.clone().multiplyScalar(0.82)
|
|
47
|
+
: new THREE.Color(options.colorGround);
|
|
28
48
|
const uniforms = {
|
|
29
49
|
uTime: { value: 0 },
|
|
30
50
|
uWindDirection: { value: normalizeWindDirection(wind.direction) },
|
|
@@ -32,9 +52,12 @@ export function createGrassMaterial(options = {}) {
|
|
|
32
52
|
uWindSpeed: { value: wind.speed },
|
|
33
53
|
uWindGustScale: { value: wind.gustScale },
|
|
34
54
|
uWindFlutter: { value: wind.flutter },
|
|
35
|
-
uColorBase: { value:
|
|
55
|
+
uColorBase: { value: colorBase },
|
|
36
56
|
uColorTip: { value: new THREE.Color(options.colorTip ?? "#a6cc58") },
|
|
57
|
+
uColorGround: { value: colorGround },
|
|
37
58
|
uColorVariation: { value: options.colorVariation ?? 0.28 },
|
|
59
|
+
uDistanceFade: { value: new THREE.Vector2(fade.start, fade.end) },
|
|
60
|
+
uNormalLift: { value: THREE.MathUtils.clamp(options.normalLift ?? 0.6, 0, 1) },
|
|
38
61
|
};
|
|
39
62
|
const material = new THREE.MeshStandardMaterial({
|
|
40
63
|
color: "#ffffff",
|
|
@@ -54,36 +77,61 @@ attribute float instanceWidth;
|
|
|
54
77
|
attribute float instanceBend;
|
|
55
78
|
attribute float instancePhase;
|
|
56
79
|
attribute float instanceColorMix;
|
|
80
|
+
attribute vec2 bladeOffset;
|
|
81
|
+
attribute vec4 bladeVary;
|
|
82
|
+
attribute float bladeBendScale;
|
|
57
83
|
uniform float uTime;
|
|
58
84
|
uniform vec2 uWindDirection;
|
|
59
85
|
uniform float uWindStrength;
|
|
60
86
|
uniform float uWindSpeed;
|
|
61
87
|
uniform float uWindGustScale;
|
|
62
88
|
uniform float uWindFlutter;
|
|
89
|
+
uniform vec2 uDistanceFade;
|
|
90
|
+
uniform float uNormalLift;
|
|
63
91
|
varying float vGrassBladeT;
|
|
64
92
|
varying float vGrassColorMix;
|
|
93
|
+
varying float vGrassFade;
|
|
65
94
|
vec3 jgGrassPosition;
|
|
66
95
|
vec3 jgGrassNormal;
|
|
67
96
|
void jgComputeGrassBlade() {
|
|
68
97
|
float t = position.y;
|
|
69
98
|
float side = position.x;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
float
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
float
|
|
99
|
+
// Camera-distance thinning: each tuft draws a random threshold, so the field dissolves
|
|
100
|
+
// progressively across the fade band instead of ending at a hard curtain.
|
|
101
|
+
float camDist = distance((modelMatrix * vec4(instanceOffset, 1.0)).xyz, cameraPosition);
|
|
102
|
+
float fadeT = uDistanceFade.y > uDistanceFade.x
|
|
103
|
+
? smoothstep(uDistanceFade.x, uDistanceFade.y, camDist)
|
|
104
|
+
: 0.0;
|
|
105
|
+
float threshold = fract(instanceColorMix * 61.7) * 0.85;
|
|
106
|
+
float keep = 1.0 - smoothstep(threshold, threshold + 0.15, fadeT);
|
|
107
|
+
float heightScale = instanceHeight * bladeVary.y * keep;
|
|
108
|
+
// Paddle taper: hold width through the mid-blade and only pinch near the tip — thin
|
|
109
|
+
// triangle blades leave the ground visible between tufts and read as stubble.
|
|
110
|
+
float taper = (1.0 - 0.3 * t) * (1.0 - smoothstep(0.5, 1.0, t) * 0.95);
|
|
111
|
+
// Surviving distant tufts widen to hold silhouette coverage as neighbors thin out.
|
|
112
|
+
float width = instanceWidth * bladeVary.z * taper * (1.0 + fadeT * 1.4);
|
|
113
|
+
float arc = instanceBend * bladeBendScale * t * t;
|
|
114
|
+
vec3 localPosition = vec3(side * width, heightScale * t - arc * heightScale * 0.18, arc * heightScale);
|
|
115
|
+
vec3 localNormal = normalize(vec3(0.0, -instanceBend * bladeBendScale * t, 1.0));
|
|
116
|
+
float bladeYaw = instanceYaw + bladeVary.x;
|
|
117
|
+
float cy = cos(bladeYaw);
|
|
118
|
+
float sy = sin(bladeYaw);
|
|
77
119
|
vec3 yawedPosition = vec3(localPosition.x * cy + localPosition.z * sy, localPosition.y, -localPosition.x * sy + localPosition.z * cy);
|
|
78
120
|
vec3 yawedNormal = normalize(vec3(localNormal.x * cy + localNormal.z * sy, localNormal.y, -localNormal.x * sy + localNormal.z * cy));
|
|
121
|
+
float ct = cos(instanceYaw);
|
|
122
|
+
float st = sin(instanceYaw);
|
|
123
|
+
vec3 tuftOffset = vec3(bladeOffset.x * ct + bladeOffset.y * st, 0.0, -bladeOffset.x * st + bladeOffset.y * ct);
|
|
79
124
|
float gustPhase = dot(instanceOffset.xz, uWindDirection) * uWindGustScale + uTime * uWindSpeed + instancePhase;
|
|
80
125
|
float gust = sin(gustPhase) * 0.7 + sin(gustPhase * 0.43 + 2.4) * 0.3;
|
|
81
|
-
float flutter = sin(uTime * 7.5 + instancePhase * 2.7) * uWindFlutter;
|
|
82
|
-
vec2 windOffset = uWindDirection * (gust * uWindStrength + flutter) * t * t;
|
|
83
|
-
jgGrassPosition = instanceOffset + yawedPosition + vec3(windOffset.x, 0.0, windOffset.y);
|
|
84
|
-
|
|
126
|
+
float flutter = sin(uTime * 7.5 + instancePhase * 2.7 + bladeVary.w * 6.2832) * uWindFlutter;
|
|
127
|
+
vec2 windOffset = uWindDirection * (gust * uWindStrength + flutter) * t * t * (0.4 + 0.6 * bladeVary.y);
|
|
128
|
+
jgGrassPosition = instanceOffset + tuftOffset + yawedPosition + vec3(windOffset.x, 0.0, windOffset.y);
|
|
129
|
+
// Lifting normals toward straight-up lights the field like a continuous meadow surface —
|
|
130
|
+
// side-lit individual blade planes read as dark stubble, not turf.
|
|
131
|
+
jgGrassNormal = normalize(mix(yawedNormal, vec3(0.0, 1.0, 0.0), uNormalLift));
|
|
85
132
|
vGrassBladeT = t;
|
|
86
133
|
vGrassColorMix = instanceColorMix;
|
|
134
|
+
vGrassFade = fadeT;
|
|
87
135
|
}
|
|
88
136
|
`)
|
|
89
137
|
.replace("#include <beginnormal_vertex>", "jgComputeGrassBlade();\nvec3 objectNormal = jgGrassNormal;")
|
|
@@ -93,15 +141,20 @@ void jgComputeGrassBlade() {
|
|
|
93
141
|
#include <common>
|
|
94
142
|
uniform vec3 uColorBase;
|
|
95
143
|
uniform vec3 uColorTip;
|
|
144
|
+
uniform vec3 uColorGround;
|
|
96
145
|
uniform float uColorVariation;
|
|
97
146
|
varying float vGrassBladeT;
|
|
98
147
|
varying float vGrassColorMix;
|
|
148
|
+
varying float vGrassFade;
|
|
99
149
|
`)
|
|
100
150
|
.replace("#include <color_fragment>", `
|
|
101
151
|
#include <color_fragment>
|
|
102
152
|
vec3 grassColor = mix(uColorBase, uColorTip, smoothstep(0.0, 1.0, vGrassBladeT));
|
|
103
153
|
grassColor *= mix(1.0 - uColorVariation, 1.0 + uColorVariation, vGrassColorMix);
|
|
104
|
-
|
|
154
|
+
// Roots take the ground color so blades grow out of the terrain instead of floating on it,
|
|
155
|
+
// and distant blades converge back toward it so the fade band blends into the mottled ground.
|
|
156
|
+
grassColor = mix(uColorGround, grassColor, smoothstep(0.0, 0.55, vGrassBladeT));
|
|
157
|
+
grassColor = mix(grassColor, uColorGround * 1.04, vGrassFade * 0.6);
|
|
105
158
|
diffuseColor.rgb = grassColor;
|
|
106
159
|
`);
|
|
107
160
|
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
/** XZ rectangle in world units — matches the shape of a terraform snapshot's `bounds`. */
|
|
3
|
+
export interface HeightfieldRect {
|
|
4
|
+
minX: number;
|
|
5
|
+
minZ: number;
|
|
6
|
+
maxX: number;
|
|
7
|
+
maxZ: number;
|
|
8
|
+
}
|
|
9
|
+
/** Per-vertex color hook for {@link displaceHeightfieldGeometry}; write the tone into `out`. */
|
|
10
|
+
export type HeightfieldColorFn = (x: number, z: number, height: number, out: THREE.Color) => void;
|
|
11
|
+
/** Options for {@link displaceHeightfieldGeometry}. */
|
|
12
|
+
export interface HeightfieldDisplaceOptions {
|
|
13
|
+
/** World-space rect the plane spans (the plane is centered on it, XZ, +Y up). */
|
|
14
|
+
bounds: HeightfieldRect;
|
|
15
|
+
/**
|
|
16
|
+
* Dirty world-space region to re-sample; only the vertex window covering it (plus a one-vertex
|
|
17
|
+
* normal ring) updates. Omit or pass null for a full rebuild.
|
|
18
|
+
*/
|
|
19
|
+
region?: HeightfieldRect | null;
|
|
20
|
+
/** When given, writes a `color` attribute (creating it on demand) via this per-vertex hook. */
|
|
21
|
+
color?: HeightfieldColorFn;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Re-samples a `PlaneGeometry(width, depth, segments, segments).rotateX(-π/2)` heightfield mesh
|
|
25
|
+
* from a `sampleHeight` field in place: vertex Y, optional vertex color, grid central-difference
|
|
26
|
+
* normals, and an analytically maintained bounding sphere. With a dirty `region`, the work is
|
|
27
|
+
* O(region vertices) — heights/colors update inside the covering vertex window, normals inside the
|
|
28
|
+
* window plus a one-vertex ring — never a whole-mesh `computeVertexNormals`/`computeBoundingSphere`
|
|
29
|
+
* pass, which is what keeps per-frame brush stamps inside the editor's frame budget. The bounding
|
|
30
|
+
* sphere derives from the fixed plane extents plus a running height range kept on
|
|
31
|
+
* `geometry.userData`; partial passes only expand it, a full pass resets it exactly.
|
|
32
|
+
* @capability heightfield-mesh-update in-place partial update of a displaced ground plane mesh
|
|
33
|
+
*/
|
|
34
|
+
export declare function displaceHeightfieldGeometry(geometry: THREE.BufferGeometry, sampleHeight: (x: number, z: number) => number, options: HeightfieldDisplaceOptions): void;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
const EXTENT_KEY = "jgHeightfieldExtent";
|
|
3
|
+
/**
|
|
4
|
+
* Re-samples a `PlaneGeometry(width, depth, segments, segments).rotateX(-π/2)` heightfield mesh
|
|
5
|
+
* from a `sampleHeight` field in place: vertex Y, optional vertex color, grid central-difference
|
|
6
|
+
* normals, and an analytically maintained bounding sphere. With a dirty `region`, the work is
|
|
7
|
+
* O(region vertices) — heights/colors update inside the covering vertex window, normals inside the
|
|
8
|
+
* window plus a one-vertex ring — never a whole-mesh `computeVertexNormals`/`computeBoundingSphere`
|
|
9
|
+
* pass, which is what keeps per-frame brush stamps inside the editor's frame budget. The bounding
|
|
10
|
+
* sphere derives from the fixed plane extents plus a running height range kept on
|
|
11
|
+
* `geometry.userData`; partial passes only expand it, a full pass resets it exactly.
|
|
12
|
+
* @capability heightfield-mesh-update in-place partial update of a displaced ground plane mesh
|
|
13
|
+
*/
|
|
14
|
+
export function displaceHeightfieldGeometry(geometry, sampleHeight, options) {
|
|
15
|
+
const position = geometry.attributes.position;
|
|
16
|
+
const normals = geometry.attributes.normal;
|
|
17
|
+
const { bounds } = options;
|
|
18
|
+
const cx = (bounds.minX + bounds.maxX) / 2;
|
|
19
|
+
const cz = (bounds.minZ + bounds.maxZ) / 2;
|
|
20
|
+
const width = Math.max(1, bounds.maxX - bounds.minX);
|
|
21
|
+
const depth = Math.max(1, bounds.maxZ - bounds.minZ);
|
|
22
|
+
// PlaneGeometry(w, d, s, s) rotated -π/2 about X is a (s+1)² row-major grid: x fastest ascending,
|
|
23
|
+
// z ascending per row (verified by heightfieldGeometry.test.ts against the three.js constructor).
|
|
24
|
+
const side = Math.round(Math.sqrt(position.count));
|
|
25
|
+
const segments = Math.max(1, side - 1);
|
|
26
|
+
const stepX = width / segments;
|
|
27
|
+
const stepZ = depth / segments;
|
|
28
|
+
let colors = geometry.attributes.color;
|
|
29
|
+
const wantColors = options.color !== undefined;
|
|
30
|
+
const colorsMissing = wantColors && (colors === undefined || colors.count !== position.count);
|
|
31
|
+
const region = options.region ?? null;
|
|
32
|
+
const full = region === null || colorsMissing;
|
|
33
|
+
if (colorsMissing) {
|
|
34
|
+
colors = new THREE.BufferAttribute(new Float32Array(position.count * 3), 3);
|
|
35
|
+
geometry.setAttribute("color", colors);
|
|
36
|
+
}
|
|
37
|
+
const clampCell = (value) => (value < 0 ? 0 : value > segments ? segments : value);
|
|
38
|
+
const col0 = full ? 0 : clampCell(Math.floor(((region.minX - bounds.minX) / width) * segments));
|
|
39
|
+
const col1 = full ? segments : clampCell(Math.ceil(((region.maxX - bounds.minX) / width) * segments));
|
|
40
|
+
const row0 = full ? 0 : clampCell(Math.floor(((region.minZ - bounds.minZ) / depth) * segments));
|
|
41
|
+
const row1 = full ? segments : clampCell(Math.ceil(((region.maxZ - bounds.minZ) / depth) * segments));
|
|
42
|
+
const tone = new THREE.Color();
|
|
43
|
+
let windowMin = Infinity;
|
|
44
|
+
let windowMax = -Infinity;
|
|
45
|
+
for (let row = row0; row <= row1; row += 1) {
|
|
46
|
+
const base = row * side;
|
|
47
|
+
for (let col = col0; col <= col1; col += 1) {
|
|
48
|
+
const index = base + col;
|
|
49
|
+
const x = position.getX(index) + cx;
|
|
50
|
+
const z = position.getZ(index) + cz;
|
|
51
|
+
const height = sampleHeight(x, z);
|
|
52
|
+
position.setY(index, height);
|
|
53
|
+
if (height < windowMin)
|
|
54
|
+
windowMin = height;
|
|
55
|
+
if (height > windowMax)
|
|
56
|
+
windowMax = height;
|
|
57
|
+
if (options.color !== undefined) {
|
|
58
|
+
options.color(x, z, height, tone);
|
|
59
|
+
colors.setXYZ(index, tone.r, tone.g, tone.b);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Grid central-difference normals over the window plus a one-vertex ring — a height change moves
|
|
64
|
+
// its neighbors' normals too. Matches smooth shading without a whole-mesh triangle pass.
|
|
65
|
+
const nc0 = Math.max(0, col0 - 1);
|
|
66
|
+
const nc1 = Math.min(segments, col1 + 1);
|
|
67
|
+
const nr0 = Math.max(0, row0 - 1);
|
|
68
|
+
const nr1 = Math.min(segments, row1 + 1);
|
|
69
|
+
for (let row = nr0; row <= nr1; row += 1) {
|
|
70
|
+
const rowD = Math.max(0, row - 1);
|
|
71
|
+
const rowU = Math.min(segments, row + 1);
|
|
72
|
+
for (let col = nc0; col <= nc1; col += 1) {
|
|
73
|
+
const colL = Math.max(0, col - 1);
|
|
74
|
+
const colR = Math.min(segments, col + 1);
|
|
75
|
+
const hL = position.getY(row * side + colL);
|
|
76
|
+
const hR = position.getY(row * side + colR);
|
|
77
|
+
const hD = position.getY(rowD * side + col);
|
|
78
|
+
const hU = position.getY(rowU * side + col);
|
|
79
|
+
const nx = -(hR - hL) / Math.max(1e-6, (colR - colL) * stepX);
|
|
80
|
+
const nz = -(hU - hD) / Math.max(1e-6, (rowU - rowD) * stepZ);
|
|
81
|
+
const inv = 1 / Math.sqrt(nx * nx + 1 + nz * nz);
|
|
82
|
+
normals.setXYZ(row * side + col, nx * inv, inv, nz * inv);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const extent = geometry.userData[EXTENT_KEY] ?? {
|
|
86
|
+
minY: Infinity,
|
|
87
|
+
maxY: -Infinity,
|
|
88
|
+
};
|
|
89
|
+
if (full) {
|
|
90
|
+
extent.minY = windowMin;
|
|
91
|
+
extent.maxY = windowMax;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
extent.minY = Math.min(extent.minY, windowMin);
|
|
95
|
+
extent.maxY = Math.max(extent.maxY, windowMax);
|
|
96
|
+
}
|
|
97
|
+
geometry.userData[EXTENT_KEY] = extent;
|
|
98
|
+
const hasExtent = extent.minY <= extent.maxY;
|
|
99
|
+
const midY = hasExtent ? (extent.minY + extent.maxY) / 2 : 0;
|
|
100
|
+
const halfY = hasExtent ? (extent.maxY - extent.minY) / 2 : 0;
|
|
101
|
+
if (geometry.boundingSphere === null)
|
|
102
|
+
geometry.boundingSphere = new THREE.Sphere();
|
|
103
|
+
geometry.boundingSphere.center.set(0, midY, 0);
|
|
104
|
+
geometry.boundingSphere.radius = Math.sqrt((width / 2) ** 2 + (depth / 2) ** 2 + halfY ** 2);
|
|
105
|
+
position.needsUpdate = true;
|
|
106
|
+
if (colors !== undefined && wantColors)
|
|
107
|
+
colors.needsUpdate = true;
|
|
108
|
+
normals.needsUpdate = true;
|
|
109
|
+
}
|
package/dist/terrain/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { CarvedTerrain, type CarvedTerrainProps } from "./CarvedTerrain.js";
|
|
|
2
2
|
export { GrassField, type GrassFieldProps } from "./GrassField.js";
|
|
3
3
|
export { ProceduralGround, type ProceduralGroundProps } from "./ProceduralGround.js";
|
|
4
4
|
export { EditableGround, type EditableGroundProps } from "./EditableGround.js";
|
|
5
|
+
export { displaceHeightfieldGeometry, type HeightfieldColorFn, type HeightfieldDisplaceOptions, type HeightfieldRect, } from "./heightfieldGeometry.js";
|
|
5
6
|
export { TerraformBrushCursor, type TerraformBrushCursorProps } from "./TerraformBrushCursor.js";
|
|
6
7
|
export { createGrassBladeGeometry, resolveGrassBladeGeometryOptions, resolveGrassRange, type GrassBladeGeometryOptions, type GrassRange, type ResolvedGrassBladeGeometryOptions, } from "./grassGeometry.js";
|
|
7
8
|
export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, type GrassMaterialHandle, type GrassMaterialOptions, type GrassShaderUniforms, type GrassWindOptions, } from "./grassMaterial.js";
|
package/dist/terrain/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { CarvedTerrain } from "./CarvedTerrain.js";
|
|
|
2
2
|
export { GrassField } from "./GrassField.js";
|
|
3
3
|
export { ProceduralGround } from "./ProceduralGround.js";
|
|
4
4
|
export { EditableGround } from "./EditableGround.js";
|
|
5
|
+
export { displaceHeightfieldGeometry, } from "./heightfieldGeometry.js";
|
|
5
6
|
export { TerraformBrushCursor } from "./TerraformBrushCursor.js";
|
|
6
7
|
export { createGrassBladeGeometry, resolveGrassBladeGeometryOptions, resolveGrassRange, } from "./grassGeometry.js";
|
|
7
8
|
export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, } from "./grassMaterial.js";
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
import type { SoilRules } from "@jgengine/core/world/soilKind";
|
|
3
|
+
/** Optional noise-eroded edge-fade config for {@link createSoilPatchMaterial} — the patch dissolves into the ground at its border. */
|
|
4
|
+
export interface SoilPatchEdge {
|
|
5
|
+
/** Patch center on XZ, world units — with `halfSize`, drives the noise-eroded edge fade. */
|
|
6
|
+
center: readonly [number, number];
|
|
7
|
+
/** Patch half extents on XZ, world units. */
|
|
8
|
+
halfSize: readonly [number, number];
|
|
9
|
+
/** Fade band width in meters from the patch border inward. Default 1.6. */
|
|
10
|
+
fade?: number;
|
|
11
|
+
/** Terrain color under the patch: the border blends its *color* toward the ground it meets, not just its alpha. Defaults to the soil base color (color blend off). */
|
|
12
|
+
groundColor?: string;
|
|
13
|
+
}
|
|
3
14
|
/**
|
|
4
15
|
* A `MeshStandardMaterial` for one authored `soil` volume: a base dirt tint with a Worley crack-line
|
|
5
16
|
* network and an FBM moss-patch overlay injected via `onBeforeCompile`, seeded from `rules.seed`. Full
|
|
6
17
|
* PBR — lit, shadowed, fogged — like the terrain detail material it borrows its noise machinery from.
|
|
7
|
-
*
|
|
18
|
+
* Pass `edge` to add a noise-eroded alpha fade at the patch border (the draped scene-kind renderer
|
|
19
|
+
* does) so the patch blends into the surrounding ground. A game never calls this directly — author a
|
|
20
|
+
* soil patch via the editor's `soil` studio kind.
|
|
8
21
|
* @internal
|
|
9
22
|
*/
|
|
10
|
-
export declare function createSoilPatchMaterial(rules: SoilRules): THREE.MeshStandardMaterial;
|
|
23
|
+
export declare function createSoilPatchMaterial(rules: SoilRules, edge?: SoilPatchEdge): THREE.MeshStandardMaterial;
|
|
@@ -53,6 +53,10 @@ vec2 jgSoilWorleyF1F2(vec2 p){
|
|
|
53
53
|
}
|
|
54
54
|
return vec2(f1, f2);
|
|
55
55
|
}
|
|
56
|
+
float jgSoilCrackMask(vec2 p, float scale){
|
|
57
|
+
vec2 f = jgSoilWorleyF1F2(p / scale);
|
|
58
|
+
return 1.0 - smoothstep(0.0, 0.065, f.y - f.x);
|
|
59
|
+
}
|
|
56
60
|
`;
|
|
57
61
|
function hashSeed(text) {
|
|
58
62
|
let h = 2166136261 >>> 0;
|
|
@@ -66,15 +70,18 @@ function hashSeed(text) {
|
|
|
66
70
|
* A `MeshStandardMaterial` for one authored `soil` volume: a base dirt tint with a Worley crack-line
|
|
67
71
|
* network and an FBM moss-patch overlay injected via `onBeforeCompile`, seeded from `rules.seed`. Full
|
|
68
72
|
* PBR — lit, shadowed, fogged — like the terrain detail material it borrows its noise machinery from.
|
|
69
|
-
*
|
|
73
|
+
* Pass `edge` to add a noise-eroded alpha fade at the patch border (the draped scene-kind renderer
|
|
74
|
+
* does) so the patch blends into the surrounding ground. A game never calls this directly — author a
|
|
75
|
+
* soil patch via the editor's `soil` studio kind.
|
|
70
76
|
* @internal
|
|
71
77
|
*/
|
|
72
|
-
export function createSoilPatchMaterial(rules) {
|
|
78
|
+
export function createSoilPatchMaterial(rules, edge) {
|
|
73
79
|
const seedOffset = hashSeed(rules.seed);
|
|
74
80
|
const material = new THREE.MeshStandardMaterial({
|
|
75
81
|
color: rules.baseColor,
|
|
76
82
|
roughness: 0.92,
|
|
77
83
|
metalness: 0,
|
|
84
|
+
...(edge === undefined ? {} : { transparent: true, polygonOffset: true, polygonOffsetFactor: -2, polygonOffsetUnits: -2 }),
|
|
78
85
|
});
|
|
79
86
|
material.onBeforeCompile = (shader) => {
|
|
80
87
|
shader.uniforms.uSoilCrackColor = { value: new THREE.Color(rules.crackColor) };
|
|
@@ -83,6 +90,12 @@ export function createSoilPatchMaterial(rules) {
|
|
|
83
90
|
shader.uniforms.uSoilMossColor = { value: new THREE.Color(rules.mossColor) };
|
|
84
91
|
shader.uniforms.uSoilMossCoverage = { value: rules.mossCoverage };
|
|
85
92
|
shader.uniforms.uSoilSeed = { value: seedOffset };
|
|
93
|
+
if (edge !== undefined) {
|
|
94
|
+
shader.uniforms.uSoilCenter = { value: new THREE.Vector2(edge.center[0], edge.center[1]) };
|
|
95
|
+
shader.uniforms.uSoilHalfSize = { value: new THREE.Vector2(edge.halfSize[0], edge.halfSize[1]) };
|
|
96
|
+
shader.uniforms.uSoilEdgeFade = { value: edge.fade ?? 1.6 };
|
|
97
|
+
shader.uniforms.uSoilGroundColor = { value: new THREE.Color(edge.groundColor ?? rules.baseColor) };
|
|
98
|
+
}
|
|
86
99
|
shader.vertexShader = shader.vertexShader.replace("#include <common>", `#include <common>
|
|
87
100
|
varying vec3 vJgSoilWorldPos;`).replace("#include <begin_vertex>", `#include <begin_vertex>
|
|
88
101
|
vJgSoilWorldPos = (modelMatrix * vec4(transformed, 1.0)).xyz;`);
|
|
@@ -94,16 +107,41 @@ uniform float uSoilCrackIntensity;
|
|
|
94
107
|
uniform vec3 uSoilMossColor;
|
|
95
108
|
uniform float uSoilMossCoverage;
|
|
96
109
|
uniform float uSoilSeed;
|
|
97
|
-
${SOIL_NOISE_GLSL}`).replace("#include <color_fragment>", `#include <color_fragment>
|
|
110
|
+
${edge === undefined ? "" : "uniform vec2 uSoilCenter;\nuniform vec2 uSoilHalfSize;\nuniform float uSoilEdgeFade;\nuniform vec3 uSoilGroundColor;\n"}${SOIL_NOISE_GLSL}`).replace("#include <color_fragment>", `#include <color_fragment>
|
|
98
111
|
vec2 jgSoilP = vJgSoilWorldPos.xz + vec2(uSoilSeed * 0.37, uSoilSeed * 0.71);
|
|
99
|
-
|
|
100
|
-
float jgSoilCrack = 1.0 - smoothstep(0.0, 0.1, jgSoilF1F2.y - jgSoilF1F2.x);
|
|
112
|
+
float jgSoilCrack = jgSoilCrackMask(jgSoilP, uSoilCrackScale);
|
|
101
113
|
vec3 jgSoilCol = mix(diffuseColor.rgb, uSoilCrackColor, jgSoilCrack * uSoilCrackIntensity);
|
|
114
|
+
// Crack floors go darker still — paired with the relief normals they read as recessed, not painted.
|
|
115
|
+
jgSoilCol *= 1.0 - jgSoilCrack * uSoilCrackIntensity * 0.4;
|
|
116
|
+
// Sun-baked plate mottling so the dirt between cracks isn't one flat swatch.
|
|
117
|
+
jgSoilCol *= mix(0.92, 1.08, jgSoilFbm(jgSoilP / 1.1));
|
|
102
118
|
float jgSoilMossN = jgSoilFbm(jgSoilP / 2.4);
|
|
103
119
|
float jgSoilMossMask = smoothstep(1.0 - uSoilMossCoverage, 1.0 - uSoilMossCoverage + 0.18, jgSoilMossN);
|
|
104
|
-
|
|
105
|
-
|
|
120
|
+
// Fine clump break-up: moss creeps in speckled colonies (and pools into the cracks), not airbrushed blobs.
|
|
121
|
+
jgSoilMossMask *= smoothstep(0.3, 0.75, jgSoilFbm(jgSoilP / 0.55));
|
|
122
|
+
jgSoilMossMask = min(1.0, jgSoilMossMask + jgSoilMossMask * jgSoilCrack * 0.8);
|
|
123
|
+
jgSoilCol = mix(jgSoilCol, uSoilMossColor * mix(0.85, 1.1, jgSoilFbm(jgSoilP / 0.9)), jgSoilMossMask);${edge === undefined
|
|
124
|
+
? "\ndiffuseColor.rgb = jgSoilCol;"
|
|
125
|
+
: `
|
|
126
|
+
// Border: color walks toward the terrain tint across the fade band (alpha alone left a decal
|
|
127
|
+
// seam — the dirt must *become* the ground it meets), then a noise-eaten alpha finishes it.
|
|
128
|
+
vec2 jgSoilEdgeD = uSoilHalfSize - abs(vJgSoilWorldPos.xz - uSoilCenter);
|
|
129
|
+
float jgSoilEdgeDist = min(jgSoilEdgeD.x, jgSoilEdgeD.y);
|
|
130
|
+
float jgSoilEdgeNoise = jgSoilFbm(jgSoilP * 0.9) * uSoilEdgeFade;
|
|
131
|
+
float jgSoilInside = clamp((jgSoilEdgeDist - jgSoilEdgeNoise) / max(uSoilEdgeFade, 0.001) + 0.55, 0.0, 1.0);
|
|
132
|
+
jgSoilCol = mix(uSoilGroundColor, jgSoilCol, smoothstep(0.0, 0.85, jgSoilInside));
|
|
133
|
+
diffuseColor.rgb = jgSoilCol;
|
|
134
|
+
diffuseColor.a *= clamp(jgSoilInside * 1.5, 0.0, 1.0);`}`).replace("#include <normal_fragment_maps>", `#include <normal_fragment_maps>
|
|
135
|
+
// Crack relief: tilt normals along the Worley gradient so crack walls catch and lose the light
|
|
136
|
+
// (finite differences over the same field that colors the cracks — they self-shadow in sync).
|
|
137
|
+
vec2 jgSoilNp = vJgSoilWorldPos.xz + vec2(uSoilSeed * 0.37, uSoilSeed * 0.71);
|
|
138
|
+
float jgSoilNe = 0.05;
|
|
139
|
+
vec2 jgSoilGrad = vec2(
|
|
140
|
+
jgSoilCrackMask(jgSoilNp + vec2(jgSoilNe, 0.0), uSoilCrackScale) - jgSoilCrackMask(jgSoilNp - vec2(jgSoilNe, 0.0), uSoilCrackScale),
|
|
141
|
+
jgSoilCrackMask(jgSoilNp + vec2(0.0, jgSoilNe), uSoilCrackScale) - jgSoilCrackMask(jgSoilNp - vec2(0.0, jgSoilNe), uSoilCrackScale)
|
|
142
|
+
) / (2.0 * jgSoilNe);
|
|
143
|
+
normal = normalize(normal + vec3(jgSoilGrad.x, 0.0, jgSoilGrad.y) * uSoilCrackIntensity * 0.2);`);
|
|
106
144
|
};
|
|
107
|
-
material.customProgramCacheKey = () => `jgengine-soil-${rules.crackScale}-${rules.crackIntensity}-${rules.mossCoverage}-${seedOffset}`;
|
|
145
|
+
material.customProgramCacheKey = () => `jgengine-soil-${rules.crackScale}-${rules.crackIntensity}-${rules.mossCoverage}-${seedOffset}-${edge === undefined ? "solid" : "faded"}`;
|
|
108
146
|
return material;
|
|
109
147
|
}
|
|
@@ -129,11 +129,24 @@ ${NOISE_GLSL}`)
|
|
|
129
129
|
vec2 jgWp = vJgWorldPos.xz;
|
|
130
130
|
float jgFine = jgFbm(jgWp / uDetailScale);
|
|
131
131
|
float jgMacro = jgFbm(jgWp / uMacroScale);
|
|
132
|
+
float jgPatch = jgFbm(jgWp / (uMacroScale * 0.35) + vec2(37.2, 11.7));
|
|
132
133
|
float jgSlope = 1.0 - clamp(vJgWorldNormal.y, 0.0, 1.0);
|
|
133
134
|
float jgH = vJgWorldPos.y;
|
|
135
|
+
// Blade-scale flecks fade with view distance so close ground reads as turf without
|
|
136
|
+
// shimmering into noise on the horizon.
|
|
137
|
+
float jgViewDist = length(vJgWorldPos - cameraPosition);
|
|
138
|
+
float jgNear = 1.0 - smoothstep(14.0, 70.0, jgViewDist);
|
|
139
|
+
float jgFleck = jgVNoise(jgWp / max(0.05, uDetailScale * 0.024));
|
|
140
|
+
float jgFleckMid = jgVNoise(jgWp / max(0.12, uDetailScale * 0.07) + vec2(51.3, 7.9));
|
|
134
141
|
vec3 jgBase = diffuseColor.rgb;
|
|
135
|
-
jgBase *= mix(1.0 - 0.
|
|
136
|
-
|
|
142
|
+
jgBase *= mix(1.0 - 0.2 * uStrength, 1.0 + 0.16 * uStrength, jgFine);
|
|
143
|
+
// Meadow patchwork: hue-shifted sun-dried sweeps and cooler lush pockets, not just darker spots.
|
|
144
|
+
jgBase = mix(jgBase, jgBase * vec3(1.14, 1.05, 0.76), smoothstep(0.5, 0.8, jgMacro) * 0.55 * uStrength);
|
|
145
|
+
jgBase = mix(jgBase, jgBase * vec3(0.78, 1.0, 0.86), smoothstep(0.52, 0.85, jgPatch) * 0.5 * uStrength);
|
|
146
|
+
// Rolling large-scale light/dark sweeps keep far hills from flattening into one tone.
|
|
147
|
+
jgBase *= 1.0 + (jgFbm(jgWp / (uMacroScale * 2.3) + vec2(9.1, 73.4)) - 0.5) * 0.22 * uStrength;
|
|
148
|
+
jgBase *= 1.0 + (jgFleck - 0.5) * 0.48 * uStrength * jgNear;
|
|
149
|
+
jgBase *= 1.0 + (jgFleckMid - 0.5) * 0.34 * uStrength;
|
|
137
150
|
vec3 jgRock = uRockColor * mix(0.72, 1.15, jgFbm(jgWp / (uDetailScale * 1.7)));
|
|
138
151
|
float jgRockW = smoothstep(uRockSlopeStart, uRockSlopeStart + 0.18, jgSlope) * uStrength;
|
|
139
152
|
vec3 jgCol = mix(jgBase, jgRock, jgRockW);
|
package/dist/water/Ocean.d.ts
CHANGED
|
@@ -2,5 +2,11 @@ import { type ThreeElements } from "@react-three/fiber";
|
|
|
2
2
|
import { type OceanConfig } from "./OceanConfig.js";
|
|
3
3
|
export interface OceanProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material"> {
|
|
4
4
|
config?: OceanConfig;
|
|
5
|
+
/**
|
|
6
|
+
* Water-column depth (m) under a point of the sheet, in the sheet's local XZ. When provided,
|
|
7
|
+
* the shader shades shallow→deep by real depth, foams and turns transparent at the shore, and
|
|
8
|
+
* damps waves as the bed shoals. Omit for an open ocean (everywhere-deep behavior).
|
|
9
|
+
*/
|
|
10
|
+
depthAt?: (x: number, z: number) => number;
|
|
5
11
|
}
|
|
6
|
-
export declare function Ocean({ config, ...meshProps }: OceanProps): import("react").JSX.Element;
|
|
12
|
+
export declare function Ocean({ config, depthAt, ...meshProps }: OceanProps): import("react").JSX.Element;
|
package/dist/water/Ocean.js
CHANGED
|
@@ -4,16 +4,29 @@ import { useEffect, useMemo, useRef } from "react";
|
|
|
4
4
|
import * as THREE from "three";
|
|
5
5
|
import { createOceanConfig } from "./OceanConfig.js";
|
|
6
6
|
import { createOceanMaterial, syncOceanMaterial } from "./OceanMaterial.js";
|
|
7
|
-
|
|
7
|
+
/** aDepth when no sampler is given: everywhere-deep, matching the pre-depth shader look. */
|
|
8
|
+
const OCEAN_DEFAULT_DEPTH = 1000;
|
|
9
|
+
function createOceanGeometry(config, depthAt) {
|
|
8
10
|
const geometry = new THREE.PlaneGeometry(config.size, config.depth, config.resolution, config.resolution);
|
|
9
11
|
geometry.rotateX(-Math.PI / 2);
|
|
12
|
+
const positions = geometry.getAttribute("position");
|
|
13
|
+
const depths = new Float32Array(positions.count);
|
|
14
|
+
if (depthAt === undefined) {
|
|
15
|
+
depths.fill(OCEAN_DEFAULT_DEPTH);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
for (let i = 0; i < positions.count; i += 1) {
|
|
19
|
+
depths[i] = Math.max(0, depthAt(positions.getX(i), positions.getZ(i)));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
geometry.setAttribute("aDepth", new THREE.BufferAttribute(depths, 1));
|
|
10
23
|
geometry.computeBoundingSphere();
|
|
11
24
|
return geometry;
|
|
12
25
|
}
|
|
13
|
-
export function Ocean({ config, ...meshProps }) {
|
|
26
|
+
export function Ocean({ config, depthAt, ...meshProps }) {
|
|
14
27
|
const resolved = useMemo(() => createOceanConfig(config), [config]);
|
|
15
28
|
const elapsedRef = useRef(0);
|
|
16
|
-
const geometry = useMemo(() => createOceanGeometry(resolved), [resolved]);
|
|
29
|
+
const geometry = useMemo(() => createOceanGeometry(resolved, depthAt), [resolved, depthAt]);
|
|
17
30
|
const material = useMemo(() => createOceanMaterial(resolved), [resolved]);
|
|
18
31
|
useEffect(() => {
|
|
19
32
|
return () => geometry.dispose();
|
|
@@ -21,12 +21,18 @@ export interface OceanColorConfig {
|
|
|
21
21
|
opacity?: number;
|
|
22
22
|
fresnelStrength?: number;
|
|
23
23
|
horizonBlend?: number;
|
|
24
|
+
/** Water depth (m) at which the body color reaches the full `deep` tone. Default 2.6. */
|
|
25
|
+
depthRange?: number;
|
|
26
|
+
/** Sun-glint sparkle intensity, 0 disables. Default 0.35. */
|
|
27
|
+
sparkle?: number;
|
|
24
28
|
}
|
|
25
29
|
export interface OceanFoamConfig {
|
|
26
30
|
crestThreshold?: number;
|
|
27
31
|
softness?: number;
|
|
28
32
|
intensity?: number;
|
|
29
33
|
coverage?: number;
|
|
34
|
+
/** Shoreline foam lick width (m of water depth it covers). Default 0.5. */
|
|
35
|
+
shoreWidth?: number;
|
|
30
36
|
}
|
|
31
37
|
export interface OceanConfig {
|
|
32
38
|
quality?: OceanQualityPreset;
|
|
@@ -59,12 +65,15 @@ export interface ResolvedOceanColorConfig {
|
|
|
59
65
|
opacity: number;
|
|
60
66
|
fresnelStrength: number;
|
|
61
67
|
horizonBlend: number;
|
|
68
|
+
depthRange: number;
|
|
69
|
+
sparkle: number;
|
|
62
70
|
}
|
|
63
71
|
export interface ResolvedOceanFoamConfig {
|
|
64
72
|
crestThreshold: number;
|
|
65
73
|
softness: number;
|
|
66
74
|
intensity: number;
|
|
67
75
|
coverage: number;
|
|
76
|
+
shoreWidth: number;
|
|
68
77
|
}
|
|
69
78
|
export interface ResolvedOceanWaveConfig {
|
|
70
79
|
amplitude: number;
|
|
@@ -28,12 +28,15 @@ export const DEFAULT_OCEAN_CONFIG = {
|
|
|
28
28
|
opacity: 0.88,
|
|
29
29
|
fresnelStrength: 0.7,
|
|
30
30
|
horizonBlend: 0.38,
|
|
31
|
+
depthRange: 2.6,
|
|
32
|
+
sparkle: 0.35,
|
|
31
33
|
},
|
|
32
34
|
foam: {
|
|
33
35
|
crestThreshold: 0.64,
|
|
34
36
|
softness: 0.22,
|
|
35
37
|
intensity: 0.8,
|
|
36
38
|
coverage: 0.45,
|
|
39
|
+
shoreWidth: 0.5,
|
|
37
40
|
},
|
|
38
41
|
waves: [],
|
|
39
42
|
};
|
|
@@ -111,6 +114,8 @@ export function createOceanConfig(patch = {}) {
|
|
|
111
114
|
opacity: clamp(finiteOr(patch.color?.opacity, DEFAULT_OCEAN_CONFIG.color.opacity), 0, 1),
|
|
112
115
|
fresnelStrength: Math.max(0, finiteOr(patch.color?.fresnelStrength, DEFAULT_OCEAN_CONFIG.color.fresnelStrength)),
|
|
113
116
|
horizonBlend: clamp(finiteOr(patch.color?.horizonBlend, DEFAULT_OCEAN_CONFIG.color.horizonBlend), 0, 1),
|
|
117
|
+
depthRange: Math.max(0.05, finiteOr(patch.color?.depthRange, DEFAULT_OCEAN_CONFIG.color.depthRange)),
|
|
118
|
+
sparkle: Math.max(0, finiteOr(patch.color?.sparkle, DEFAULT_OCEAN_CONFIG.color.sparkle)),
|
|
114
119
|
},
|
|
115
120
|
foam: {
|
|
116
121
|
...DEFAULT_OCEAN_CONFIG.foam,
|
|
@@ -119,6 +124,7 @@ export function createOceanConfig(patch = {}) {
|
|
|
119
124
|
softness: clamp(finiteOr(patch.foam?.softness, DEFAULT_OCEAN_CONFIG.foam.softness), 0.001, 1),
|
|
120
125
|
intensity: Math.max(0, finiteOr(patch.foam?.intensity, DEFAULT_OCEAN_CONFIG.foam.intensity)),
|
|
121
126
|
coverage: clamp(finiteOr(patch.foam?.coverage, DEFAULT_OCEAN_CONFIG.foam.coverage), 0, 1),
|
|
127
|
+
shoreWidth: Math.max(0, finiteOr(patch.foam?.shoreWidth, DEFAULT_OCEAN_CONFIG.foam.shoreWidth)),
|
|
122
128
|
},
|
|
123
129
|
};
|
|
124
130
|
const defaultWaves = createDefaultWaves(withoutWaves);
|
|
@@ -46,6 +46,18 @@ export interface OceanMaterialUniforms {
|
|
|
46
46
|
uFoamCoverage: {
|
|
47
47
|
value: number;
|
|
48
48
|
};
|
|
49
|
+
uDepthRange: {
|
|
50
|
+
value: number;
|
|
51
|
+
};
|
|
52
|
+
uShoreFoamWidth: {
|
|
53
|
+
value: number;
|
|
54
|
+
};
|
|
55
|
+
uSparkle: {
|
|
56
|
+
value: number;
|
|
57
|
+
};
|
|
58
|
+
uSunDirection: {
|
|
59
|
+
value: THREE.Vector3;
|
|
60
|
+
};
|
|
49
61
|
}
|
|
50
62
|
export type OceanShaderMaterial = THREE.ShaderMaterial & {
|
|
51
63
|
uniforms: OceanMaterialUniforms;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
import { buildOceanWaveUniforms } from "./OceanConfig.js";
|
|
3
3
|
import { oceanFragmentShader, oceanVertexShader } from "./OceanShader.js";
|
|
4
|
+
/** Matches the day-sky sun heading closely enough for stylized glints — not a lighting contract. */
|
|
5
|
+
const OCEAN_SUN_DIRECTION = new THREE.Vector3(0.45, 0.78, 0.35).normalize();
|
|
4
6
|
export function createOceanMaterial(config) {
|
|
5
7
|
const material = new THREE.ShaderMaterial({
|
|
6
8
|
vertexShader: oceanVertexShader,
|
|
@@ -24,6 +26,10 @@ export function createOceanMaterial(config) {
|
|
|
24
26
|
uFoamSoftness: { value: config.foam.softness },
|
|
25
27
|
uFoamIntensity: { value: config.foam.intensity },
|
|
26
28
|
uFoamCoverage: { value: config.foam.coverage },
|
|
29
|
+
uDepthRange: { value: config.color.depthRange },
|
|
30
|
+
uShoreFoamWidth: { value: config.foam.shoreWidth },
|
|
31
|
+
uSparkle: { value: config.color.sparkle },
|
|
32
|
+
uSunDirection: { value: OCEAN_SUN_DIRECTION.clone() },
|
|
27
33
|
},
|
|
28
34
|
});
|
|
29
35
|
syncOceanMaterial(material, config, 0);
|
|
@@ -46,6 +52,9 @@ export function syncOceanMaterial(material, config, elapsedSeconds) {
|
|
|
46
52
|
material.uniforms.uFoamSoftness.value = config.foam.softness;
|
|
47
53
|
material.uniforms.uFoamIntensity.value = config.foam.intensity;
|
|
48
54
|
material.uniforms.uFoamCoverage.value = config.foam.coverage;
|
|
55
|
+
material.uniforms.uDepthRange.value = config.color.depthRange;
|
|
56
|
+
material.uniforms.uShoreFoamWidth.value = config.foam.shoreWidth;
|
|
57
|
+
material.uniforms.uSparkle.value = config.color.sparkle;
|
|
49
58
|
material.transparent = config.color.opacity < 1;
|
|
50
59
|
material.depthWrite = config.color.opacity >= 0.98;
|
|
51
60
|
}
|