@jgengine/shell 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/dist/CombatPresentation.d.ts +5 -0
- package/dist/CombatPresentation.js +8 -0
- package/dist/GameHost.d.ts +25 -1
- package/dist/GameHost.js +60 -4
- package/dist/GamePlayer.d.ts +5 -0
- package/dist/GamePlayer.js +5 -0
- package/dist/GamePlayerShell.d.ts +5 -0
- package/dist/GamePlayerShell.js +48 -358
- 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/cameraRigs.d.ts +3 -1
- package/dist/camera/cameraRigs.js +21 -3
- package/dist/camera/rigMath.d.ts +20 -0
- package/dist/camera/rigMath.js +29 -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 +12 -3
- 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 +35 -0
- package/dist/presentationResolve.js +51 -0
- package/dist/registry.d.ts +1 -0
- package/dist/render/assetBase.d.ts +17 -0
- package/dist/render/assetBase.js +35 -0
- package/dist/render/modelLoad.js +4 -1
- package/dist/render/resolveModel.d.ts +15 -0
- package/dist/render/resolveModel.js +37 -2
- 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/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/WorldScene.js +29 -2
- 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 +7 -4
- package/skills/jgengine/api.md +172 -79
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +15 -10
- package/skills/jgengine-ui/api.md +202 -47
- package/skills/jgengine-ui/capabilities.md +71 -2
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +20 -7
- package/skills/jgengine-ui/references/visual-scorecard.md +1 -1
- package/skills/jgengine-world/SKILL.md +13 -0
- package/skills/jgengine-world/api.md +419 -15
- package/skills/jgengine-world/capabilities.md +108 -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
|
@@ -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
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const oceanVertexShader = "\nuniform float uTime;\nuniform vec2 uWaveDirections[6];\nuniform vec4 uWaveParams[6];\nuniform float uChoppiness;\nuniform float uFoamThreshold;\nuniform float uFoamSoftness;\nuniform float uFoamCoverage;\n\nvarying vec3 vWorldPosition;\nvarying vec3 vNormal;\nvarying float vCrest;\nvarying float vWaveHeight;\n\nvoid main() {\n vec3 displaced = position;\n vec3 tangent = vec3(1.0, 0.0, 0.0);\n vec3 bitangent = vec3(0.0, 0.0, 1.0);\n float crest = 0.0;\n\n for (int i = 0; i < 6; i++) {\n vec2 direction = normalize(uWaveDirections[i]);\n vec4 wave = uWaveParams[i];\n float k = wave.x;\n float amplitude = wave.y;\n float steepness = wave.z;\n float omega = wave.w;\n float phase = k * dot(direction, position.xz) - omega * uTime;\n float sine = sin(phase);\n float cosine = cos(phase);\n float horizontal = steepness * amplitude * uChoppiness;\n displaced.x += horizontal * direction.x * cosine;\n displaced.z += horizontal * direction.y * cosine;\n displaced.y += amplitude * sine;\n float slopeTerm = horizontal * k * sine;\n tangent += vec3(-slopeTerm * direction.x * direction.x, amplitude * k * direction.x * cosine, -slopeTerm * direction.x * direction.y);\n bitangent += vec3(-slopeTerm * direction.x * direction.y, amplitude * k * direction.y * cosine, -slopeTerm * direction.y * direction.y);\n crest = max(crest, smoothstep(uFoamThreshold, uFoamThreshold + uFoamSoftness, sine * steepness + uFoamCoverage * 0.35));\n }\n\n vNormal = normalize(cross(bitangent, tangent));\n vCrest = crest;\n vWaveHeight = displaced.y;\n vec4 worldPosition = modelMatrix * vec4(displaced, 1.0);\n vWorldPosition = worldPosition.xyz;\n gl_Position = projectionMatrix * viewMatrix * worldPosition;\n}\n";
|
|
2
|
-
export declare const oceanFragmentShader = "\nuniform vec3 uShallowColor;\nuniform vec3 uDeepColor;\nuniform vec3 uCrestColor;\nuniform vec3 uFoamColor;\nuniform float uOpacity;\nuniform float uFresnelStrength;\nuniform float uHorizonBlend;\nuniform float uFoamIntensity;\n\nvarying vec3 vWorldPosition;\nvarying vec3 vNormal;\nvarying float vCrest;\nvarying float vWaveHeight;\n\
|
|
1
|
+
export declare const oceanVertexShader = "\nuniform float uTime;\nuniform vec2 uWaveDirections[6];\nuniform vec4 uWaveParams[6];\nuniform float uChoppiness;\nuniform float uFoamThreshold;\nuniform float uFoamSoftness;\nuniform float uFoamCoverage;\n\nattribute float aDepth;\n\nvarying vec3 vWorldPosition;\nvarying vec3 vNormal;\nvarying float vCrest;\nvarying float vWaveHeight;\nvarying float vDepth;\nvarying vec2 vOceanUv;\n\nvoid main() {\n vOceanUv = uv;\n vec3 displaced = position;\n vec3 tangent = vec3(1.0, 0.0, 0.0);\n vec3 bitangent = vec3(0.0, 0.0, 1.0);\n float crest = 0.0;\n // Waves flatten as the bed shoals so swells die at the shoreline instead of clipping it.\n float depthDamp = smoothstep(0.05, 1.4, aDepth);\n\n for (int i = 0; i < 6; i++) {\n vec2 direction = normalize(uWaveDirections[i]);\n vec4 wave = uWaveParams[i];\n float k = wave.x;\n float amplitude = wave.y * depthDamp;\n float steepness = wave.z;\n float omega = wave.w;\n float phase = k * dot(direction, position.xz) - omega * uTime;\n float sine = sin(phase);\n float cosine = cos(phase);\n float horizontal = steepness * amplitude * uChoppiness;\n displaced.x += horizontal * direction.x * cosine;\n displaced.z += horizontal * direction.y * cosine;\n displaced.y += amplitude * sine;\n float slopeTerm = horizontal * k * sine;\n tangent += vec3(-slopeTerm * direction.x * direction.x, amplitude * k * direction.x * cosine, -slopeTerm * direction.x * direction.y);\n bitangent += vec3(-slopeTerm * direction.x * direction.y, amplitude * k * direction.y * cosine, -slopeTerm * direction.y * direction.y);\n crest = max(crest, smoothstep(uFoamThreshold, uFoamThreshold + uFoamSoftness, sine * steepness + uFoamCoverage * 0.35));\n }\n\n vNormal = normalize(cross(bitangent, tangent));\n vCrest = crest;\n vWaveHeight = displaced.y;\n vDepth = aDepth;\n vec4 worldPosition = modelMatrix * vec4(displaced, 1.0);\n vWorldPosition = worldPosition.xyz;\n gl_Position = projectionMatrix * viewMatrix * worldPosition;\n}\n";
|
|
2
|
+
export declare const oceanFragmentShader = "\nuniform float uTime;\nuniform vec3 uShallowColor;\nuniform vec3 uDeepColor;\nuniform vec3 uCrestColor;\nuniform vec3 uFoamColor;\nuniform float uOpacity;\nuniform float uFresnelStrength;\nuniform float uHorizonBlend;\nuniform float uFoamIntensity;\nuniform float uDepthRange;\nuniform float uShoreFoamWidth;\nuniform float uSparkle;\nuniform vec3 uSunDirection;\n\nvarying vec3 vWorldPosition;\nvarying vec3 vNormal;\nvarying float vCrest;\nvarying float vWaveHeight;\nvarying float vDepth;\nvarying vec2 vOceanUv;\n\nfloat jgWaterHash(vec2 p) {\n uvec2 q = uvec2(ivec2(floor(p)));\n uint x = q.x * 1664525u + q.y * 1013904223u;\n x = (x ^ (x >> 16u)) * 2246822519u;\n x = (x ^ (x >> 13u)) * 3266489917u;\n return float(x ^ (x >> 16u)) * (1.0 / 4294967296.0);\n}\n\nfloat jgWaterNoise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n float a = jgWaterHash(i);\n float b = jgWaterHash(i + vec2(1.0, 0.0));\n float c = jgWaterHash(i + vec2(0.0, 1.0));\n float d = jgWaterHash(i + vec2(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nvoid main() {\n vec3 viewDirection = normalize(cameraPosition - vWorldPosition);\n\n // Fine scrolling normal perturbation: two counter-drifting noise layers ripple the surface\n // between the big Gerstner swells.\n vec2 wp = vWorldPosition.xz;\n float ripple1 = jgWaterNoise(wp * 1.7 + vec2(uTime * 0.22, uTime * 0.15));\n float ripple2 = jgWaterNoise(wp * 3.1 - vec2(uTime * 0.17, uTime * 0.26));\n vec3 normal = normalize(normalize(vNormal) + vec3(ripple1 - 0.5, 0.0, ripple2 - 0.5) * 0.14);\n\n // Depth-based body color: warm shallows at the shore ramp falling to the deep tone.\n float depthMix = smoothstep(0.0, max(uDepthRange, 0.001), vDepth);\n vec3 waterColor = mix(uShallowColor, uDeepColor, depthMix);\n waterColor = mix(waterColor, uCrestColor, smoothstep(0.15, 1.2, vWaveHeight) * 0.22);\n\n // Restrained fresnel: a soft sky tint at grazing angles, never a white-out.\n float fresnel = pow(1.0 - max(dot(normal, viewDirection), 0.0), 5.0);\n waterColor = mix(waterColor, vec3(0.62, 0.78, 0.85), min(fresnel * uFresnelStrength, 0.45));\n float horizon = smoothstep(0.0, 1.0, 1.0 - abs(normal.y));\n waterColor = mix(waterColor, uShallowColor, horizon * uHorizonBlend * 0.6);\n\n // Sun glints: sparkle where the perturbed normal mirrors the sun, gated by noise so it\n // twinkles in cells instead of sheening uniformly.\n vec3 halfway = normalize(viewDirection + normalize(uSunDirection));\n float glint = pow(max(dot(normal, halfway), 0.0), 150.0);\n float glintMask = smoothstep(0.68, 0.92, jgWaterNoise(wp * 2.3 + vec2(uTime * 0.4, -uTime * 0.31)));\n waterColor += vec3(1.0, 0.98, 0.9) * glint * glintMask * uSparkle;\n\n // Foam: a noise-eaten shoreline lick plus crest foam. A second, larger noise gates whole\n // stretches of shoreline so foam laps in patches instead of outlining the bank uniformly.\n float foamNoise = jgWaterNoise(wp * 2.6 + vec2(uTime * 0.28, uTime * 0.2));\n float foamStretch = smoothstep(0.3, 0.8, jgWaterNoise(wp * 0.33 + vec2(3.7, 9.2) + vec2(uTime * 0.05, 0.0)));\n float shoreFoam = (1.0 - smoothstep(0.0, max(uShoreFoamWidth, 0.001), vDepth - foamNoise * uShoreFoamWidth * 0.9))\n * smoothstep(0.25, 0.75, foamNoise) * (0.2 + 0.6 * foamStretch);\n float crestFoam = clamp(vCrest * uFoamIntensity, 0.0, 1.0) * smoothstep(0.35, 0.75, foamNoise);\n float foam = clamp(shoreFoam + crestFoam, 0.0, 1.0);\n vec3 color = mix(waterColor, uFoamColor, foam);\n\n // Soft shore: the sheet turns glass-clear over the last stretch of shoaling bed, so the\n // waterline is a blend into the terrain, not a hard tarp edge.\n float shoreAlpha = smoothstep(0.0, 0.55, vDepth);\n float alpha = uOpacity * mix(0.35, 1.0, shoreAlpha) + foam * 0.3;\n // The sheet's own border always dissolves \u2014 a bounded water plane must never end in a\n // visible straight cut, whatever the bed underneath is doing.\n float borderDist = min(min(vOceanUv.x, 1.0 - vOceanUv.x), min(vOceanUv.y, 1.0 - vOceanUv.y));\n alpha *= smoothstep(0.0, 0.045, borderDist);\n gl_FragColor = vec4(color, clamp(alpha, 0.0, 1.0));\n}\n";
|
|
@@ -7,22 +7,29 @@ uniform float uFoamThreshold;
|
|
|
7
7
|
uniform float uFoamSoftness;
|
|
8
8
|
uniform float uFoamCoverage;
|
|
9
9
|
|
|
10
|
+
attribute float aDepth;
|
|
11
|
+
|
|
10
12
|
varying vec3 vWorldPosition;
|
|
11
13
|
varying vec3 vNormal;
|
|
12
14
|
varying float vCrest;
|
|
13
15
|
varying float vWaveHeight;
|
|
16
|
+
varying float vDepth;
|
|
17
|
+
varying vec2 vOceanUv;
|
|
14
18
|
|
|
15
19
|
void main() {
|
|
20
|
+
vOceanUv = uv;
|
|
16
21
|
vec3 displaced = position;
|
|
17
22
|
vec3 tangent = vec3(1.0, 0.0, 0.0);
|
|
18
23
|
vec3 bitangent = vec3(0.0, 0.0, 1.0);
|
|
19
24
|
float crest = 0.0;
|
|
25
|
+
// Waves flatten as the bed shoals so swells die at the shoreline instead of clipping it.
|
|
26
|
+
float depthDamp = smoothstep(0.05, 1.4, aDepth);
|
|
20
27
|
|
|
21
28
|
for (int i = 0; i < 6; i++) {
|
|
22
29
|
vec2 direction = normalize(uWaveDirections[i]);
|
|
23
30
|
vec4 wave = uWaveParams[i];
|
|
24
31
|
float k = wave.x;
|
|
25
|
-
float amplitude = wave.y;
|
|
32
|
+
float amplitude = wave.y * depthDamp;
|
|
26
33
|
float steepness = wave.z;
|
|
27
34
|
float omega = wave.w;
|
|
28
35
|
float phase = k * dot(direction, position.xz) - omega * uTime;
|
|
@@ -41,12 +48,14 @@ void main() {
|
|
|
41
48
|
vNormal = normalize(cross(bitangent, tangent));
|
|
42
49
|
vCrest = crest;
|
|
43
50
|
vWaveHeight = displaced.y;
|
|
51
|
+
vDepth = aDepth;
|
|
44
52
|
vec4 worldPosition = modelMatrix * vec4(displaced, 1.0);
|
|
45
53
|
vWorldPosition = worldPosition.xyz;
|
|
46
54
|
gl_Position = projectionMatrix * viewMatrix * worldPosition;
|
|
47
55
|
}
|
|
48
56
|
`;
|
|
49
57
|
export const oceanFragmentShader = `
|
|
58
|
+
uniform float uTime;
|
|
50
59
|
uniform vec3 uShallowColor;
|
|
51
60
|
uniform vec3 uDeepColor;
|
|
52
61
|
uniform vec3 uCrestColor;
|
|
@@ -55,24 +64,83 @@ uniform float uOpacity;
|
|
|
55
64
|
uniform float uFresnelStrength;
|
|
56
65
|
uniform float uHorizonBlend;
|
|
57
66
|
uniform float uFoamIntensity;
|
|
67
|
+
uniform float uDepthRange;
|
|
68
|
+
uniform float uShoreFoamWidth;
|
|
69
|
+
uniform float uSparkle;
|
|
70
|
+
uniform vec3 uSunDirection;
|
|
58
71
|
|
|
59
72
|
varying vec3 vWorldPosition;
|
|
60
73
|
varying vec3 vNormal;
|
|
61
74
|
varying float vCrest;
|
|
62
75
|
varying float vWaveHeight;
|
|
76
|
+
varying float vDepth;
|
|
77
|
+
varying vec2 vOceanUv;
|
|
78
|
+
|
|
79
|
+
float jgWaterHash(vec2 p) {
|
|
80
|
+
uvec2 q = uvec2(ivec2(floor(p)));
|
|
81
|
+
uint x = q.x * 1664525u + q.y * 1013904223u;
|
|
82
|
+
x = (x ^ (x >> 16u)) * 2246822519u;
|
|
83
|
+
x = (x ^ (x >> 13u)) * 3266489917u;
|
|
84
|
+
return float(x ^ (x >> 16u)) * (1.0 / 4294967296.0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
float jgWaterNoise(vec2 p) {
|
|
88
|
+
vec2 i = floor(p);
|
|
89
|
+
vec2 f = fract(p);
|
|
90
|
+
vec2 u = f * f * (3.0 - 2.0 * f);
|
|
91
|
+
float a = jgWaterHash(i);
|
|
92
|
+
float b = jgWaterHash(i + vec2(1.0, 0.0));
|
|
93
|
+
float c = jgWaterHash(i + vec2(0.0, 1.0));
|
|
94
|
+
float d = jgWaterHash(i + vec2(1.0, 1.0));
|
|
95
|
+
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
|
96
|
+
}
|
|
63
97
|
|
|
64
98
|
void main() {
|
|
65
|
-
vec3 normal = normalize(vNormal);
|
|
66
99
|
vec3 viewDirection = normalize(cameraPosition - vWorldPosition);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
100
|
+
|
|
101
|
+
// Fine scrolling normal perturbation: two counter-drifting noise layers ripple the surface
|
|
102
|
+
// between the big Gerstner swells.
|
|
103
|
+
vec2 wp = vWorldPosition.xz;
|
|
104
|
+
float ripple1 = jgWaterNoise(wp * 1.7 + vec2(uTime * 0.22, uTime * 0.15));
|
|
105
|
+
float ripple2 = jgWaterNoise(wp * 3.1 - vec2(uTime * 0.17, uTime * 0.26));
|
|
106
|
+
vec3 normal = normalize(normalize(vNormal) + vec3(ripple1 - 0.5, 0.0, ripple2 - 0.5) * 0.14);
|
|
107
|
+
|
|
108
|
+
// Depth-based body color: warm shallows at the shore ramp falling to the deep tone.
|
|
109
|
+
float depthMix = smoothstep(0.0, max(uDepthRange, 0.001), vDepth);
|
|
110
|
+
vec3 waterColor = mix(uShallowColor, uDeepColor, depthMix);
|
|
111
|
+
waterColor = mix(waterColor, uCrestColor, smoothstep(0.15, 1.2, vWaveHeight) * 0.22);
|
|
112
|
+
|
|
113
|
+
// Restrained fresnel: a soft sky tint at grazing angles, never a white-out.
|
|
114
|
+
float fresnel = pow(1.0 - max(dot(normal, viewDirection), 0.0), 5.0);
|
|
115
|
+
waterColor = mix(waterColor, vec3(0.62, 0.78, 0.85), min(fresnel * uFresnelStrength, 0.45));
|
|
72
116
|
float horizon = smoothstep(0.0, 1.0, 1.0 - abs(normal.y));
|
|
73
|
-
waterColor = mix(waterColor, uShallowColor, horizon * uHorizonBlend);
|
|
74
|
-
|
|
117
|
+
waterColor = mix(waterColor, uShallowColor, horizon * uHorizonBlend * 0.6);
|
|
118
|
+
|
|
119
|
+
// Sun glints: sparkle where the perturbed normal mirrors the sun, gated by noise so it
|
|
120
|
+
// twinkles in cells instead of sheening uniformly.
|
|
121
|
+
vec3 halfway = normalize(viewDirection + normalize(uSunDirection));
|
|
122
|
+
float glint = pow(max(dot(normal, halfway), 0.0), 150.0);
|
|
123
|
+
float glintMask = smoothstep(0.68, 0.92, jgWaterNoise(wp * 2.3 + vec2(uTime * 0.4, -uTime * 0.31)));
|
|
124
|
+
waterColor += vec3(1.0, 0.98, 0.9) * glint * glintMask * uSparkle;
|
|
125
|
+
|
|
126
|
+
// Foam: a noise-eaten shoreline lick plus crest foam. A second, larger noise gates whole
|
|
127
|
+
// stretches of shoreline so foam laps in patches instead of outlining the bank uniformly.
|
|
128
|
+
float foamNoise = jgWaterNoise(wp * 2.6 + vec2(uTime * 0.28, uTime * 0.2));
|
|
129
|
+
float foamStretch = smoothstep(0.3, 0.8, jgWaterNoise(wp * 0.33 + vec2(3.7, 9.2) + vec2(uTime * 0.05, 0.0)));
|
|
130
|
+
float shoreFoam = (1.0 - smoothstep(0.0, max(uShoreFoamWidth, 0.001), vDepth - foamNoise * uShoreFoamWidth * 0.9))
|
|
131
|
+
* smoothstep(0.25, 0.75, foamNoise) * (0.2 + 0.6 * foamStretch);
|
|
132
|
+
float crestFoam = clamp(vCrest * uFoamIntensity, 0.0, 1.0) * smoothstep(0.35, 0.75, foamNoise);
|
|
133
|
+
float foam = clamp(shoreFoam + crestFoam, 0.0, 1.0);
|
|
75
134
|
vec3 color = mix(waterColor, uFoamColor, foam);
|
|
76
|
-
|
|
135
|
+
|
|
136
|
+
// Soft shore: the sheet turns glass-clear over the last stretch of shoaling bed, so the
|
|
137
|
+
// waterline is a blend into the terrain, not a hard tarp edge.
|
|
138
|
+
float shoreAlpha = smoothstep(0.0, 0.55, vDepth);
|
|
139
|
+
float alpha = uOpacity * mix(0.35, 1.0, shoreAlpha) + foam * 0.3;
|
|
140
|
+
// The sheet's own border always dissolves — a bounded water plane must never end in a
|
|
141
|
+
// visible straight cut, whatever the bed underneath is doing.
|
|
142
|
+
float borderDist = min(min(vOceanUv.x, 1.0 - vOceanUv.x), min(vOceanUv.y, 1.0 - vOceanUv.y));
|
|
143
|
+
alpha *= smoothstep(0.0, 0.045, borderDist);
|
|
144
|
+
gl_FragColor = vec4(color, clamp(alpha, 0.0, 1.0));
|
|
77
145
|
}
|
|
78
146
|
`;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PlaceWorldFeature } from "@jgengine/core/world/place";
|
|
2
|
+
/**
|
|
3
|
+
* The substrate of a place world, rendered plainly: a flat slab or a round planet under the engine
|
|
4
|
+
* default sky. This is deliberately not a diorama — sky look, foliage, props, and sculpt are scene
|
|
5
|
+
* content the editor authors; `voxel` grounds are realized by the game's generation systems and
|
|
6
|
+
* `board` grounds by the game's own 2D face, so both mount nothing here.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export declare function PlaceScene({ feature }: {
|
|
10
|
+
feature: PlaceWorldFeature;
|
|
11
|
+
}): import("react").JSX.Element | null;
|