@jgengine/shell 0.22.0 → 0.24.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 +110 -0
- package/dist/GamePlayerShell.js +18 -3
- package/dist/GameUiPreview.js +2 -1
- package/dist/Shell3dPresentation.js +13 -8
- package/dist/ShellChrome.d.ts +1 -1
- package/dist/ShellChrome.js +4 -2
- package/dist/ShellHudPresentation.js +2 -1
- package/dist/ShellNotices.d.ts +26 -0
- package/dist/ShellNotices.js +51 -0
- package/dist/audio/AudioComponents.d.ts +3 -1
- package/dist/audio/AudioComponents.js +5 -2
- package/dist/audio/listenerAnchor.d.ts +15 -0
- package/dist/audio/listenerAnchor.js +17 -0
- package/dist/audio/playableAudio.d.ts +9 -0
- package/dist/audio/playableAudio.js +11 -0
- package/dist/commandSink.d.ts +1 -1
- package/dist/commandSink.js +8 -3
- package/dist/defineGame.js +3 -1
- package/dist/devtools/DevtoolsOverlay.js +1 -0
- package/dist/devtools/agentBridge.d.ts +21 -0
- package/dist/devtools/agentBridge.js +95 -3
- package/dist/devtools/devtoolsReports.d.ts +32 -0
- package/dist/devtools/devtoolsReports.js +26 -0
- package/dist/devtools/simStepper.d.ts +21 -0
- package/dist/devtools/simStepper.js +37 -0
- package/dist/drivers/FrameDriver.js +26 -14
- package/dist/drivers/HudOnlyDriver.js +16 -5
- package/dist/environment/EnvironmentScene.js +19 -7
- package/dist/environment/RoadJunctions.d.ts +3 -1
- package/dist/environment/RoadJunctions.js +9 -2
- package/dist/environment/RoadRibbons.d.ts +3 -1
- package/dist/environment/RoadRibbons.js +16 -3
- package/dist/environment/terrainGroundColor.js +3 -3
- package/dist/materialOverride.js +4 -2
- package/dist/playSurfaceFocus.d.ts +16 -0
- package/dist/playSurfaceFocus.js +42 -0
- package/dist/postfx/PostProcessing.d.ts +3 -1
- package/dist/postfx/PostProcessing.js +14 -6
- package/dist/registry.d.ts +5 -1
- package/dist/render/CascadedShadows.d.ts +2 -0
- package/dist/render/CascadedShadows.js +4 -0
- package/dist/render/EntityPreview.js +1 -1
- package/dist/render/EnvironmentLighting.d.ts +26 -7
- package/dist/render/EnvironmentLighting.js +40 -27
- package/dist/render/SceneLighting.js +2 -2
- package/dist/render/modelLoad.d.ts +7 -0
- package/dist/render/modelLoad.js +13 -0
- package/dist/render/modelRender.d.ts +1 -0
- package/dist/render/modelRender.js +3 -0
- package/dist/render/modelSurface.d.ts +39 -0
- package/dist/render/modelSurface.js +206 -0
- package/dist/render/useModelInstance.d.ts +1 -1
- package/dist/render/useModelInstance.js +2 -2
- package/dist/scatter/scatterModels.js +3 -1
- package/dist/scene/AuthoredScene.d.ts +11 -3
- package/dist/scene/AuthoredScene.js +17 -9
- package/dist/scene/CityRenderer.js +142 -25
- package/dist/scene/cityJunctionSurface.d.ts +7 -0
- package/dist/scene/cityJunctionSurface.js +34 -0
- package/dist/settings/settingsController.js +1 -1
- package/dist/terrain/groundEdgeFade.d.ts +67 -0
- package/dist/terrain/groundEdgeFade.js +234 -0
- package/dist/terrain/index.d.ts +5 -0
- package/dist/terrain/index.js +5 -0
- package/dist/terrain/surfaceRibbonMaterial.d.ts +62 -0
- package/dist/terrain/surfaceRibbonMaterial.js +227 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +33 -10
- package/dist/terrain/terrainDetailMaterial.js +149 -63
- package/dist/terrain/terrainPaintColor.d.ts +12 -3
- package/dist/terrain/terrainPaintColor.js +46 -1
- package/dist/terrain/terrainQuality.d.ts +5 -0
- package/dist/terrain/terrainQuality.js +7 -0
- package/dist/terrain/terrainSurfaceShader.d.ts +16 -0
- package/dist/terrain/terrainSurfaceShader.js +266 -0
- package/dist/terrain/terrainSurfaces.d.ts +59 -0
- package/dist/terrain/terrainSurfaces.js +199 -0
- package/dist/world/StaticObjectModels.d.ts +1 -1
- package/dist/world/StaticObjectModels.js +1 -1
- package/dist/world/WorldScene.js +1 -1
- package/dist/world/staticObjectBatches.d.ts +5 -1
- package/dist/world/staticObjectBatches.js +9 -5
- package/dist/world/worldBarSamples.d.ts +13 -1
- package/dist/world/worldBarSamples.js +40 -8
- package/package.json +5 -5
- package/skills/jgengine/api.md +18 -18
- package/skills/jgengine-ui/SKILL.md +4 -0
- package/skills/jgengine-ui/api.md +254 -37
- package/skills/jgengine-ui/capabilities.md +58 -0
- package/skills/jgengine-ui/reference-primitives.md +4 -4
- package/skills/jgengine-world/api.md +27 -14
- package/skills/jgengine-world/capabilities.md +4 -0
- package/skills/jgengine-world/reference.md +27 -8
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface for a crossing's welded patch: gravel only when every street running through the crossing
|
|
3
|
+
* is gravel, so an unpaved hamlet's junctions stay gravel while a lane meeting a paved main stays asphalt.
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
export function junctionSurface(cross, streets) {
|
|
7
|
+
let touched = false;
|
|
8
|
+
for (const street of streets) {
|
|
9
|
+
const reach = cross.radius + street.width * 0.5 + 0.5;
|
|
10
|
+
if (distanceToPath(cross.x, cross.z, street.points) > reach)
|
|
11
|
+
continue;
|
|
12
|
+
if (street.surface !== "gravel")
|
|
13
|
+
return "asphalt";
|
|
14
|
+
touched = true;
|
|
15
|
+
}
|
|
16
|
+
return touched ? "gravel" : "asphalt";
|
|
17
|
+
}
|
|
18
|
+
function distanceToPath(x, z, points) {
|
|
19
|
+
if (points.length === 0)
|
|
20
|
+
return Infinity;
|
|
21
|
+
if (points.length === 1)
|
|
22
|
+
return Math.hypot(x - points[0][0], z - points[0][1]);
|
|
23
|
+
let best = Infinity;
|
|
24
|
+
for (let i = 1; i < points.length; i += 1) {
|
|
25
|
+
const [ax, az] = points[i - 1];
|
|
26
|
+
const [bx, bz] = points[i];
|
|
27
|
+
const dx = bx - ax;
|
|
28
|
+
const dz = bz - az;
|
|
29
|
+
const len2 = dx * dx + dz * dz;
|
|
30
|
+
const t = len2 === 0 ? 0 : Math.max(0, Math.min(1, ((x - ax) * dx + (z - az) * dz) / len2));
|
|
31
|
+
best = Math.min(best, Math.hypot(x - (ax + dx * t), z - (az + dz * t)));
|
|
32
|
+
}
|
|
33
|
+
return best;
|
|
34
|
+
}
|
|
@@ -51,7 +51,7 @@ export function useSettingsCategories(config) {
|
|
|
51
51
|
const id = busVolumeSettingId(bus.id);
|
|
52
52
|
return {
|
|
53
53
|
id,
|
|
54
|
-
label: `${bus.id.charAt(0).toUpperCase()}${bus.id.slice(1)} volume`,
|
|
54
|
+
label: bus.id === "ui" ? "UI volume" : `${bus.id.charAt(0).toUpperCase()}${bus.id.slice(1)} volume`,
|
|
55
55
|
kind: "slider",
|
|
56
56
|
value: store.get(id, bus.gain ?? 1),
|
|
57
57
|
min: 0,
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
/** Options for {@link buildGroundCoverageMask}. */
|
|
3
|
+
export interface GroundCoverageOptions {
|
|
4
|
+
/** Metres inside the outer edge over which ground goes from clear to opaque. */
|
|
5
|
+
width: number;
|
|
6
|
+
/** Gaps between layers narrower than twice this (metres) count as inside, not as an outer edge. Default 1.5. */
|
|
7
|
+
closeRadius?: number;
|
|
8
|
+
/** Finest cell size in metres. Default 0.5. */
|
|
9
|
+
cell?: number;
|
|
10
|
+
/** Upper bound on cells per axis; large districts get coarser cells. Default 640. */
|
|
11
|
+
maxCells?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* How far each point of a merged ground footprint lies inside its outer edge, as a grid over world
|
|
15
|
+
* XZ: `data[row * columns + column]` is 0 on or outside the edge and 255 at `width` metres or more
|
|
16
|
+
* inside. Cell centres sit at `origin + (index + 0.5) * cell`.
|
|
17
|
+
*/
|
|
18
|
+
export interface GroundCoverageMask {
|
|
19
|
+
data: Uint8Array;
|
|
20
|
+
columns: number;
|
|
21
|
+
rows: number;
|
|
22
|
+
originX: number;
|
|
23
|
+
originZ: number;
|
|
24
|
+
cell: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Rasterize the XZ footprint of ground meshes (roads, block land, parks…) into a
|
|
28
|
+
* {@link GroundCoverageMask}. Small gaps between layers are closed first, so only the merged
|
|
29
|
+
* footprint's outer edge fades: seams between layers inside it keep their full opacity. Vertical
|
|
30
|
+
* faces (curbs) add nothing. Returns `null` when the meshes cover nothing.
|
|
31
|
+
* @capability ground-edge-fade fade merged ground layers into the terrain at their outer edge, keeping inner seams crisp
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildGroundCoverageMask(geometries: readonly (THREE.BufferGeometry | null | undefined)[], options: GroundCoverageOptions): GroundCoverageMask | null;
|
|
34
|
+
/** Shared uniforms for {@link applyGroundEdgeFade}; `setMask` retargets every material using them without a recompile. */
|
|
35
|
+
export interface GroundEdgeFade {
|
|
36
|
+
readonly uniforms: {
|
|
37
|
+
uJgFadeMask: {
|
|
38
|
+
value: THREE.Texture;
|
|
39
|
+
};
|
|
40
|
+
/** `(originX, originZ, columns * cell, rows * cell)`. */
|
|
41
|
+
uJgFadeBounds: {
|
|
42
|
+
value: THREE.Vector4;
|
|
43
|
+
};
|
|
44
|
+
/** Mask width in metres; scales the noise that breaks up the edge. */
|
|
45
|
+
uJgFadeWidth: {
|
|
46
|
+
value: number;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
/** Swap in a new mask (`null` = fully opaque everywhere). The previous texture is disposed. */
|
|
50
|
+
setMask(mask: GroundCoverageMask | null, width: number): void;
|
|
51
|
+
dispose(): void;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Uniforms for one faded footprint, shared by every ground material of one district. Starts fully opaque.
|
|
55
|
+
* @capability ground-edge-fade fade merged ground layers into the terrain at their outer edge, keeping inner seams crisp
|
|
56
|
+
*/
|
|
57
|
+
export declare function createGroundEdgeFade(): GroundEdgeFade;
|
|
58
|
+
/**
|
|
59
|
+
* Fade a ground material out at the outer edge of a {@link GroundCoverageMask}: clear on the edge,
|
|
60
|
+
* opaque `width` metres inside, with a noise-broken line. Seams between layers inside the
|
|
61
|
+
* footprint are untouched, so a district keeps crisp road/plaza edges while its outer boundary
|
|
62
|
+
* melts into the terrain. Turns on blending (depth writes stay on); give stacked layers ascending
|
|
63
|
+
* `renderOrder` so lower ones draw first. Chains any existing `onBeforeCompile`, so it composes
|
|
64
|
+
* with `createSurfaceRibbonMaterial` and plain `MeshStandardMaterial`s alike.
|
|
65
|
+
* @capability ground-edge-fade fade merged ground layers into the terrain at their outer edge, keeping inner seams crisp
|
|
66
|
+
*/
|
|
67
|
+
export declare function applyGroundEdgeFade<M extends THREE.Material>(material: M, fade: GroundEdgeFade): M;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
const DIAGONAL = Math.SQRT2;
|
|
3
|
+
/** Two-pass chamfer distance (in cells) from every cell to the nearest seed cell. */
|
|
4
|
+
function chamfer(seed, columns, rows) {
|
|
5
|
+
const d = new Float32Array(columns * rows);
|
|
6
|
+
for (let i = 0; i < d.length; i += 1)
|
|
7
|
+
d[i] = seed[i] === 1 ? 0 : Infinity;
|
|
8
|
+
for (let j = 0; j < rows; j += 1) {
|
|
9
|
+
for (let i = 0; i < columns; i += 1) {
|
|
10
|
+
const k = j * columns + i;
|
|
11
|
+
let best = d[k];
|
|
12
|
+
if (i > 0)
|
|
13
|
+
best = Math.min(best, d[k - 1] + 1);
|
|
14
|
+
if (j > 0) {
|
|
15
|
+
best = Math.min(best, d[k - columns] + 1);
|
|
16
|
+
if (i > 0)
|
|
17
|
+
best = Math.min(best, d[k - columns - 1] + DIAGONAL);
|
|
18
|
+
if (i + 1 < columns)
|
|
19
|
+
best = Math.min(best, d[k - columns + 1] + DIAGONAL);
|
|
20
|
+
}
|
|
21
|
+
d[k] = best;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
for (let j = rows - 1; j >= 0; j -= 1) {
|
|
25
|
+
for (let i = columns - 1; i >= 0; i -= 1) {
|
|
26
|
+
const k = j * columns + i;
|
|
27
|
+
let best = d[k];
|
|
28
|
+
if (i + 1 < columns)
|
|
29
|
+
best = Math.min(best, d[k + 1] + 1);
|
|
30
|
+
if (j + 1 < rows) {
|
|
31
|
+
best = Math.min(best, d[k + columns] + 1);
|
|
32
|
+
if (i + 1 < columns)
|
|
33
|
+
best = Math.min(best, d[k + columns + 1] + DIAGONAL);
|
|
34
|
+
if (i > 0)
|
|
35
|
+
best = Math.min(best, d[k + columns - 1] + DIAGONAL);
|
|
36
|
+
}
|
|
37
|
+
d[k] = best;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return d;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Rasterize the XZ footprint of ground meshes (roads, block land, parks…) into a
|
|
44
|
+
* {@link GroundCoverageMask}. Small gaps between layers are closed first, so only the merged
|
|
45
|
+
* footprint's outer edge fades: seams between layers inside it keep their full opacity. Vertical
|
|
46
|
+
* faces (curbs) add nothing. Returns `null` when the meshes cover nothing.
|
|
47
|
+
* @capability ground-edge-fade fade merged ground layers into the terrain at their outer edge, keeping inner seams crisp
|
|
48
|
+
*/
|
|
49
|
+
export function buildGroundCoverageMask(geometries, options) {
|
|
50
|
+
const width = Math.max(1e-3, options.width);
|
|
51
|
+
const closeRadius = Math.max(0, options.closeRadius ?? 1.5);
|
|
52
|
+
let minX = Infinity;
|
|
53
|
+
let minZ = Infinity;
|
|
54
|
+
let maxX = -Infinity;
|
|
55
|
+
let maxZ = -Infinity;
|
|
56
|
+
for (const geometry of geometries) {
|
|
57
|
+
const position = geometry?.getAttribute("position");
|
|
58
|
+
if (position === undefined)
|
|
59
|
+
continue;
|
|
60
|
+
for (let v = 0; v < position.count; v += 1) {
|
|
61
|
+
const x = position.getX(v);
|
|
62
|
+
const z = position.getZ(v);
|
|
63
|
+
if (x < minX)
|
|
64
|
+
minX = x;
|
|
65
|
+
if (x > maxX)
|
|
66
|
+
maxX = x;
|
|
67
|
+
if (z < minZ)
|
|
68
|
+
minZ = z;
|
|
69
|
+
if (z > maxZ)
|
|
70
|
+
maxZ = z;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!Number.isFinite(minX))
|
|
74
|
+
return null;
|
|
75
|
+
const maxCells = Math.max(8, Math.floor(options.maxCells ?? 640));
|
|
76
|
+
const pad = closeRadius + 2;
|
|
77
|
+
const spanX = maxX - minX + pad * 2;
|
|
78
|
+
const spanZ = maxZ - minZ + pad * 2;
|
|
79
|
+
const cell = Math.max(options.cell ?? 0.5, spanX / maxCells, spanZ / maxCells);
|
|
80
|
+
const columns = Math.max(2, Math.ceil(spanX / cell));
|
|
81
|
+
const rows = Math.max(2, Math.ceil(spanZ / cell));
|
|
82
|
+
const originX = minX - pad;
|
|
83
|
+
const originZ = minZ - pad;
|
|
84
|
+
const covered = new Uint8Array(columns * rows);
|
|
85
|
+
let any = false;
|
|
86
|
+
for (const geometry of geometries) {
|
|
87
|
+
const position = geometry?.getAttribute("position");
|
|
88
|
+
if (geometry == null || position === undefined)
|
|
89
|
+
continue;
|
|
90
|
+
const index = geometry.getIndex();
|
|
91
|
+
const count = index === null ? position.count : index.count;
|
|
92
|
+
for (let t = 0; t + 2 < count; t += 3) {
|
|
93
|
+
const a = index === null ? t : index.getX(t);
|
|
94
|
+
const b = index === null ? t + 1 : index.getX(t + 1);
|
|
95
|
+
const c = index === null ? t + 2 : index.getX(t + 2);
|
|
96
|
+
const ax = position.getX(a);
|
|
97
|
+
const az = position.getZ(a);
|
|
98
|
+
const bx = position.getX(b);
|
|
99
|
+
const bz = position.getZ(b);
|
|
100
|
+
const cx = position.getX(c);
|
|
101
|
+
const cz = position.getZ(c);
|
|
102
|
+
const area = (bx - ax) * (cz - az) - (bz - az) * (cx - ax);
|
|
103
|
+
if (Math.abs(area) < 1e-6)
|
|
104
|
+
continue;
|
|
105
|
+
const sign = area > 0 ? 1 : -1;
|
|
106
|
+
const i0 = Math.max(0, Math.floor((Math.min(ax, bx, cx) - originX) / cell - 0.5));
|
|
107
|
+
const i1 = Math.min(columns - 1, Math.ceil((Math.max(ax, bx, cx) - originX) / cell - 0.5));
|
|
108
|
+
const j0 = Math.max(0, Math.floor((Math.min(az, bz, cz) - originZ) / cell - 0.5));
|
|
109
|
+
const j1 = Math.min(rows - 1, Math.ceil((Math.max(az, bz, cz) - originZ) / cell - 0.5));
|
|
110
|
+
for (let j = j0; j <= j1; j += 1) {
|
|
111
|
+
const pz = originZ + (j + 0.5) * cell;
|
|
112
|
+
for (let i = i0; i <= i1; i += 1) {
|
|
113
|
+
const px = originX + (i + 0.5) * cell;
|
|
114
|
+
const e0 = ((bx - ax) * (pz - az) - (bz - az) * (px - ax)) * sign;
|
|
115
|
+
const e1 = ((cx - bx) * (pz - bz) - (cz - bz) * (px - bx)) * sign;
|
|
116
|
+
const e2 = ((ax - cx) * (pz - cz) - (az - cz) * (px - cx)) * sign;
|
|
117
|
+
if (e0 >= 0 && e1 >= 0 && e2 >= 0) {
|
|
118
|
+
covered[j * columns + i] = 1;
|
|
119
|
+
any = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!any)
|
|
126
|
+
return null;
|
|
127
|
+
// Morphological closing: grow the footprint by `closeRadius`, then measure inward from the grown
|
|
128
|
+
// edge less that radius, so a narrow gap between two layers never reads as an outer edge.
|
|
129
|
+
const toCovered = chamfer(covered, columns, rows);
|
|
130
|
+
const outsideGrown = new Uint8Array(columns * rows);
|
|
131
|
+
for (let k = 0; k < outsideGrown.length; k += 1)
|
|
132
|
+
outsideGrown[k] = toCovered[k] * cell > closeRadius ? 1 : 0;
|
|
133
|
+
const toOutside = chamfer(outsideGrown, columns, rows);
|
|
134
|
+
const data = new Uint8Array(columns * rows);
|
|
135
|
+
for (let k = 0; k < data.length; k += 1) {
|
|
136
|
+
// Centre-to-centre distance overshoots the edge between two cells by half a cell.
|
|
137
|
+
const inside = (toOutside[k] - 0.5) * cell - closeRadius;
|
|
138
|
+
data[k] = Math.round(Math.max(0, Math.min(1, inside / width)) * 255);
|
|
139
|
+
}
|
|
140
|
+
return { data, columns, rows, originX, originZ, cell };
|
|
141
|
+
}
|
|
142
|
+
function opaqueTexture() {
|
|
143
|
+
const texture = new THREE.DataTexture(new Uint8Array([255]), 1, 1, THREE.RedFormat);
|
|
144
|
+
texture.needsUpdate = true;
|
|
145
|
+
return texture;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Uniforms for one faded footprint, shared by every ground material of one district. Starts fully opaque.
|
|
149
|
+
* @capability ground-edge-fade fade merged ground layers into the terrain at their outer edge, keeping inner seams crisp
|
|
150
|
+
*/
|
|
151
|
+
export function createGroundEdgeFade() {
|
|
152
|
+
const uniforms = {
|
|
153
|
+
uJgFadeMask: { value: opaqueTexture() },
|
|
154
|
+
uJgFadeBounds: { value: new THREE.Vector4(-1e6, -1e6, 2e6, 2e6) },
|
|
155
|
+
uJgFadeWidth: { value: 1 },
|
|
156
|
+
};
|
|
157
|
+
return {
|
|
158
|
+
uniforms,
|
|
159
|
+
setMask(mask, width) {
|
|
160
|
+
uniforms.uJgFadeMask.value.dispose();
|
|
161
|
+
if (mask === null) {
|
|
162
|
+
uniforms.uJgFadeMask.value = opaqueTexture();
|
|
163
|
+
uniforms.uJgFadeBounds.value.set(-1e6, -1e6, 2e6, 2e6);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const texture = new THREE.DataTexture(mask.data, mask.columns, mask.rows, THREE.RedFormat);
|
|
167
|
+
texture.magFilter = THREE.LinearFilter;
|
|
168
|
+
texture.minFilter = THREE.LinearFilter;
|
|
169
|
+
texture.wrapS = THREE.ClampToEdgeWrapping;
|
|
170
|
+
texture.wrapT = THREE.ClampToEdgeWrapping;
|
|
171
|
+
// One byte per texel: rows are not 4-byte aligned unless `columns` happens to be.
|
|
172
|
+
texture.unpackAlignment = 1;
|
|
173
|
+
texture.needsUpdate = true;
|
|
174
|
+
uniforms.uJgFadeMask.value = texture;
|
|
175
|
+
uniforms.uJgFadeBounds.value.set(mask.originX, mask.originZ, mask.columns * mask.cell, mask.rows * mask.cell);
|
|
176
|
+
}
|
|
177
|
+
uniforms.uJgFadeWidth.value = Math.max(1e-3, width);
|
|
178
|
+
},
|
|
179
|
+
dispose() {
|
|
180
|
+
uniforms.uJgFadeMask.value.dispose();
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const FADE_FRAGMENT_GLSL = /* glsl */ `
|
|
185
|
+
{
|
|
186
|
+
float jgFadeIn = texture2D(uJgFadeMask, (vJgFadePos - uJgFadeBounds.xy) / uJgFadeBounds.zw).r;
|
|
187
|
+
// Two octaves of value noise keep the edge from reading as a traced outline.
|
|
188
|
+
float jgFadeN = jgFadeNoise(vJgFadePos / max(0.6, uJgFadeWidth * 1.2)) * 0.7 + jgFadeNoise(vJgFadePos / max(0.25, uJgFadeWidth * 0.3) + 17.3) * 0.3 - 0.5;
|
|
189
|
+
diffuseColor.a *= smoothstep(0.0, 1.0, (jgFadeIn - 0.15 + jgFadeN * 0.35) / 0.7);
|
|
190
|
+
}
|
|
191
|
+
`;
|
|
192
|
+
const FADE_NOISE_GLSL = /* glsl */ `
|
|
193
|
+
float jgFadeHash(vec2 p) {
|
|
194
|
+
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
|
|
195
|
+
}
|
|
196
|
+
float jgFadeNoise(vec2 p) {
|
|
197
|
+
vec2 i = floor(p);
|
|
198
|
+
vec2 f = fract(p);
|
|
199
|
+
vec2 u = f * f * (3.0 - 2.0 * f);
|
|
200
|
+
return mix(mix(jgFadeHash(i), jgFadeHash(i + vec2(1.0, 0.0)), u.x), mix(jgFadeHash(i + vec2(0.0, 1.0)), jgFadeHash(i + vec2(1.0, 1.0)), u.x), u.y);
|
|
201
|
+
}
|
|
202
|
+
`;
|
|
203
|
+
/**
|
|
204
|
+
* Fade a ground material out at the outer edge of a {@link GroundCoverageMask}: clear on the edge,
|
|
205
|
+
* opaque `width` metres inside, with a noise-broken line. Seams between layers inside the
|
|
206
|
+
* footprint are untouched, so a district keeps crisp road/plaza edges while its outer boundary
|
|
207
|
+
* melts into the terrain. Turns on blending (depth writes stay on); give stacked layers ascending
|
|
208
|
+
* `renderOrder` so lower ones draw first. Chains any existing `onBeforeCompile`, so it composes
|
|
209
|
+
* with `createSurfaceRibbonMaterial` and plain `MeshStandardMaterial`s alike.
|
|
210
|
+
* @capability ground-edge-fade fade merged ground layers into the terrain at their outer edge, keeping inner seams crisp
|
|
211
|
+
*/
|
|
212
|
+
export function applyGroundEdgeFade(material, fade) {
|
|
213
|
+
const previousCompile = material.onBeforeCompile;
|
|
214
|
+
const previousKey = material.customProgramCacheKey;
|
|
215
|
+
material.transparent = true;
|
|
216
|
+
material.onBeforeCompile = (shader, renderer) => {
|
|
217
|
+
previousCompile.call(material, shader, renderer);
|
|
218
|
+
Object.assign(shader.uniforms, fade.uniforms);
|
|
219
|
+
shader.vertexShader = shader.vertexShader
|
|
220
|
+
.replace("#include <common>", "#include <common>\nvarying vec2 vJgFadePos;")
|
|
221
|
+
.replace("#include <begin_vertex>", "#include <begin_vertex>\nvJgFadePos = (modelMatrix * vec4(transformed, 1.0)).xz;");
|
|
222
|
+
shader.fragmentShader = shader.fragmentShader
|
|
223
|
+
.replace("#include <common>", `#include <common>
|
|
224
|
+
varying vec2 vJgFadePos;
|
|
225
|
+
uniform sampler2D uJgFadeMask;
|
|
226
|
+
uniform vec4 uJgFadeBounds;
|
|
227
|
+
uniform float uJgFadeWidth;
|
|
228
|
+
${FADE_NOISE_GLSL}`)
|
|
229
|
+
.replace("#include <alphamap_fragment>", `#include <alphamap_fragment>\n${FADE_FRAGMENT_GLSL}`);
|
|
230
|
+
};
|
|
231
|
+
material.customProgramCacheKey = () => `${previousKey.call(material)}-jgedgefade`;
|
|
232
|
+
material.needsUpdate = true;
|
|
233
|
+
return material;
|
|
234
|
+
}
|
package/dist/terrain/index.d.ts
CHANGED
|
@@ -10,3 +10,8 @@ export { createSeededRandom, hashNoise2, seedToUint32, type TerrainSeed } from "
|
|
|
10
10
|
export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, normalizeHeightBlend, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, type FieldGroundOptions, type ProceduralTerrainConfig, type ResolvedTerrainSegments, type ResolvedTerrainSize, type TerrainArea, type TerrainHeightSampler, type TerrainSurfaceColorOptions, type TerrainVertexColorOptions, } from "./terrainMath.js";
|
|
11
11
|
export { arenaField, flatField, fractalNoise, noiseField, resolveGroundStep, resolveTerrainField, valueNoise, withNormal, type FractalNoiseConfig, type NoiseFieldConfig, type TerrainField, type TerrainNormal, } from "@jgengine/core/world/terrain";
|
|
12
12
|
export { createTerrainPaintColorSampler, type TerrainPaintColorSampler, type TerrainPaintSource } from "./terrainPaintColor.js";
|
|
13
|
+
export { ALL_TERRAIN_SURFACE_CLASSES, createTerrainDetailMaterial, type TerrainDetailMaterialHandle, type TerrainDetailMaterialOptions, type TerrainDetailMaterialTextures, } from "./terrainDetailMaterial.js";
|
|
14
|
+
export { createTerrainPaintTextures, MAX_TERRAIN_PAINT_LAYERS, packTerrainPaintWeights, terrainSurfaceClass, terrainSurfaceClassIndex, terrainSurfaceRoughness, type TerrainPaintSlot, type TerrainPaintTextures, type TerrainPaintWeights, type TerrainSurfaceClass, } from "./terrainSurfaces.js";
|
|
15
|
+
export { TerrainQualityContext, useTerrainQuality } from "./terrainQuality.js";
|
|
16
|
+
export { createSurfaceRibbonMaterial, resolvePathSurface, RIBBON_FRAME_ATTRIBUTE, ribbonEdgeMargin, ribbonSurfaceFrame, useSurfaceRibbonMaterial, type RibbonSurfaceStyle, type SurfaceRibbonMaterialOptions, } from "./surfaceRibbonMaterial.js";
|
|
17
|
+
export { applyGroundEdgeFade, buildGroundCoverageMask, createGroundEdgeFade, type GroundCoverageMask, type GroundCoverageOptions, type GroundEdgeFade, } from "./groundEdgeFade.js";
|
package/dist/terrain/index.js
CHANGED
|
@@ -10,3 +10,8 @@ export { createSeededRandom, hashNoise2, seedToUint32 } from "./random.js";
|
|
|
10
10
|
export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, normalizeHeightBlend, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, } from "./terrainMath.js";
|
|
11
11
|
export { arenaField, flatField, fractalNoise, noiseField, resolveGroundStep, resolveTerrainField, valueNoise, withNormal, } from "@jgengine/core/world/terrain";
|
|
12
12
|
export { createTerrainPaintColorSampler } from "./terrainPaintColor.js";
|
|
13
|
+
export { ALL_TERRAIN_SURFACE_CLASSES, createTerrainDetailMaterial, } from "./terrainDetailMaterial.js";
|
|
14
|
+
export { createTerrainPaintTextures, MAX_TERRAIN_PAINT_LAYERS, packTerrainPaintWeights, terrainSurfaceClass, terrainSurfaceClassIndex, terrainSurfaceRoughness, } from "./terrainSurfaces.js";
|
|
15
|
+
export { TerrainQualityContext, useTerrainQuality } from "./terrainQuality.js";
|
|
16
|
+
export { createSurfaceRibbonMaterial, resolvePathSurface, RIBBON_FRAME_ATTRIBUTE, ribbonEdgeMargin, ribbonSurfaceFrame, useSurfaceRibbonMaterial, } from "./surfaceRibbonMaterial.js";
|
|
17
|
+
export { applyGroundEdgeFade, buildGroundCoverageMask, createGroundEdgeFade, } from "./groundEdgeFade.js";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type { GraphicsQuality } from "@jgengine/core/settings/settingsModel";
|
|
3
|
+
/** Vertex attribute {@link ribbonSurfaceFrame} fills: metres along the ribbon, signed metres across it, metres to its far end. */
|
|
4
|
+
export declare const RIBBON_FRAME_ATTRIBUTE = "jgRibbon";
|
|
5
|
+
/** How a ground ribbon (path, road, sidewalk) shades: procedural surface, edge blend and wear. */
|
|
6
|
+
export interface RibbonSurfaceStyle {
|
|
7
|
+
/** Surface id classified like terrain paint (`"dirt"`, `"gravel"`, `"asphalt"`, …). */
|
|
8
|
+
surface: string;
|
|
9
|
+
/** Width of the edge fade in metres, centred on the authored edge. 0 keeps a hard opaque edge. */
|
|
10
|
+
feather: number;
|
|
11
|
+
/** How far noise pushes the edge in or out, in metres. */
|
|
12
|
+
warp: number;
|
|
13
|
+
/** Wheel ruts on soft ground, polished tyre lanes on pavement, 0..1. */
|
|
14
|
+
wear: number;
|
|
15
|
+
}
|
|
16
|
+
/** Options for {@link createSurfaceRibbonMaterial}. */
|
|
17
|
+
export interface SurfaceRibbonMaterialOptions extends Partial<RibbonSurfaceStyle> {
|
|
18
|
+
/** Authored hue; the procedural surface varies around it. */
|
|
19
|
+
color: string;
|
|
20
|
+
/** Authored half width in metres; the edge fade is centred on it. */
|
|
21
|
+
halfWidth: number;
|
|
22
|
+
/** Override the surface class's default roughness. */
|
|
23
|
+
roughness?: number;
|
|
24
|
+
/** Pattern frequency multiplier. Default 1. */
|
|
25
|
+
scale?: number;
|
|
26
|
+
/** Graphics tier, from `useTerrainQuality()`. `low` drops micro-normals and fine octaves. Default `high`. */
|
|
27
|
+
quality?: GraphicsQuality;
|
|
28
|
+
/** Pull the ribbon toward the camera in depth so it wins over a coplanar layer beneath. */
|
|
29
|
+
polygonOffset?: boolean;
|
|
30
|
+
/** Multiply by the geometry's `color` attribute, as city ground meshes carry per-vertex tints. */
|
|
31
|
+
vertexColors?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Default ribbon look for an authored path: the surface comes from `meta.surface`, else the path
|
|
35
|
+
* kind (`"road"` paves, `"gravel-track"` gravels), else dirt. Soft ground gets a noise-warped edge
|
|
36
|
+
* that fades into the terrain and wheel ruts; pavement keeps a near-hard edge and faint tyre lanes.
|
|
37
|
+
* `meta.surfaceDetail: false` (or `meta.surface: false`) returns `null` for the old flat ribbon;
|
|
38
|
+
* `meta.edgeSoftness` scales the fade (0 = hard) and `meta.wear` sets wear 0..1.
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolvePathSurface(kind: string, width: number, meta: Readonly<Record<string, unknown>> | undefined): RibbonSurfaceStyle | null;
|
|
41
|
+
/** Extra ribbon width (metres, both sides together) a style needs so its fade straddles the authored edge. */
|
|
42
|
+
export declare function ribbonEdgeMargin(style: Pick<RibbonSurfaceStyle, "feather" | "warp">): number;
|
|
43
|
+
/**
|
|
44
|
+
* Per-vertex `(along, across, toEnd)` for a ribbon in `buildRoadRibbon`'s layout (left/right vertex
|
|
45
|
+
* pairs down the centreline). Pass several ribbons' position arrays to get one concatenated frame
|
|
46
|
+
* for a merged mesh; each ribbon's distances restart at its own start.
|
|
47
|
+
*/
|
|
48
|
+
export declare function ribbonSurfaceFrame(ribbons: readonly Float32Array[]): Float32Array;
|
|
49
|
+
/**
|
|
50
|
+
* A `MeshStandardMaterial` that shades a ground ribbon (path, road, sidewalk) with the terrain's own
|
|
51
|
+
* procedural surface functions, so a dirt track or asphalt strip matches painted ground: the
|
|
52
|
+
* authored colour varied by the surface class's grain, roughness and micro-normal, wheel ruts or tyre
|
|
53
|
+
* lanes, and (with `feather > 0`) a noise-warped edge that fades into the terrain. The geometry needs
|
|
54
|
+
* a {@link RIBBON_FRAME_ATTRIBUTE} attribute from {@link ribbonSurfaceFrame}; build it wider by
|
|
55
|
+
* {@link ribbonEdgeMargin} so the fade straddles the authored edge. The caller disposes the material.
|
|
56
|
+
*/
|
|
57
|
+
export declare function createSurfaceRibbonMaterial(options: SurfaceRibbonMaterialOptions): THREE.MeshStandardMaterial;
|
|
58
|
+
/**
|
|
59
|
+
* {@link createSurfaceRibbonMaterial} for a React mesh: rebuilt only when an option changes and
|
|
60
|
+
* disposed on unmount. Pass `null` to get `null` (the caller falls back to a flat material).
|
|
61
|
+
*/
|
|
62
|
+
export declare function useSurfaceRibbonMaterial(options: SurfaceRibbonMaterialOptions | null): THREE.MeshStandardMaterial | null;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { useEffect, useMemo } from "react";
|
|
2
|
+
import * as THREE from "three";
|
|
3
|
+
import { NOISE_GLSL, SURFACE_BUMP_NORMAL_GLSL, SURFACE_FUNCTIONS_GLSL } from "./terrainSurfaceShader.js";
|
|
4
|
+
import { terrainSurfaceClass, terrainSurfaceClassIndex, terrainSurfaceRoughness } from "./terrainSurfaces.js";
|
|
5
|
+
/** Vertex attribute {@link ribbonSurfaceFrame} fills: metres along the ribbon, signed metres across it, metres to its far end. */
|
|
6
|
+
export const RIBBON_FRAME_ATTRIBUTE = "jgRibbon";
|
|
7
|
+
const SOFT_GROUND = new Set(["dirt", "mud", "sand", "gravel", "grass", "ground", "snow"]);
|
|
8
|
+
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
9
|
+
/**
|
|
10
|
+
* Default ribbon look for an authored path: the surface comes from `meta.surface`, else the path
|
|
11
|
+
* kind (`"road"` paves, `"gravel-track"` gravels), else dirt. Soft ground gets a noise-warped edge
|
|
12
|
+
* that fades into the terrain and wheel ruts; pavement keeps a near-hard edge and faint tyre lanes.
|
|
13
|
+
* `meta.surfaceDetail: false` (or `meta.surface: false`) returns `null` for the old flat ribbon;
|
|
14
|
+
* `meta.edgeSoftness` scales the fade (0 = hard) and `meta.wear` sets wear 0..1.
|
|
15
|
+
*/
|
|
16
|
+
export function resolvePathSurface(kind, width, meta) {
|
|
17
|
+
if (meta?.["surfaceDetail"] === false || meta?.["surface"] === false)
|
|
18
|
+
return null;
|
|
19
|
+
const id = typeof meta?.["surface"] === "string" ? meta["surface"] : kind;
|
|
20
|
+
const classified = terrainSurfaceClass(id);
|
|
21
|
+
const cls = classified === "ground" ? "dirt" : classified;
|
|
22
|
+
const surface = classified === "ground" ? "dirt" : id;
|
|
23
|
+
const soft = SOFT_GROUND.has(cls);
|
|
24
|
+
const softness = typeof meta?.["edgeSoftness"] === "number" ? Math.max(0, meta["edgeSoftness"]) : 1;
|
|
25
|
+
const feather = (soft ? clamp(width * 0.18, 0.35, 1.6) : clamp(width * 0.04, 0.08, 0.3)) * softness;
|
|
26
|
+
const warp = (soft ? clamp(width * 0.08, 0.15, 0.6) : 0.03) * softness;
|
|
27
|
+
const wear = typeof meta?.["wear"] === "number" ? clamp(meta["wear"], 0, 1) : soft ? 1 : 0.6;
|
|
28
|
+
return { surface, feather, warp, wear };
|
|
29
|
+
}
|
|
30
|
+
/** Extra ribbon width (metres, both sides together) a style needs so its fade straddles the authored edge. */
|
|
31
|
+
export function ribbonEdgeMargin(style) {
|
|
32
|
+
return style.feather + style.warp * 2;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Per-vertex `(along, across, toEnd)` for a ribbon in `buildRoadRibbon`'s layout (left/right vertex
|
|
36
|
+
* pairs down the centreline). Pass several ribbons' position arrays to get one concatenated frame
|
|
37
|
+
* for a merged mesh; each ribbon's distances restart at its own start.
|
|
38
|
+
*/
|
|
39
|
+
export function ribbonSurfaceFrame(ribbons) {
|
|
40
|
+
const total = ribbons.reduce((sum, positions) => sum + positions.length, 0);
|
|
41
|
+
const out = new Float32Array(total);
|
|
42
|
+
let offset = 0;
|
|
43
|
+
for (const p of ribbons) {
|
|
44
|
+
const pairs = Math.floor(p.length / 6);
|
|
45
|
+
let along = 0;
|
|
46
|
+
let px = 0;
|
|
47
|
+
let pz = 0;
|
|
48
|
+
for (let i = 0; i < pairs; i++) {
|
|
49
|
+
const b = i * 6;
|
|
50
|
+
const cx = (p[b] + p[b + 3]) / 2;
|
|
51
|
+
const cz = (p[b + 2] + p[b + 5]) / 2;
|
|
52
|
+
if (i > 0)
|
|
53
|
+
along += Math.hypot(cx - px, cz - pz);
|
|
54
|
+
px = cx;
|
|
55
|
+
pz = cz;
|
|
56
|
+
const half = Math.hypot(p[b] - p[b + 3], p[b + 2] - p[b + 5]) / 2;
|
|
57
|
+
out[offset + b] = along;
|
|
58
|
+
out[offset + b + 1] = half;
|
|
59
|
+
out[offset + b + 3] = along;
|
|
60
|
+
out[offset + b + 4] = -half;
|
|
61
|
+
}
|
|
62
|
+
for (let i = 0; i < pairs; i++) {
|
|
63
|
+
const b = offset + i * 6;
|
|
64
|
+
out[b + 2] = along - out[b];
|
|
65
|
+
out[b + 5] = along - out[b + 3];
|
|
66
|
+
}
|
|
67
|
+
offset += p.length;
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
const RIBBON_FRAGMENT_GLSL = /* glsl */ `
|
|
72
|
+
vec2 jgWp = vJgWorldPos.xz;
|
|
73
|
+
jgFootprint = max(length(dFdx(vJgWorldPos)), length(dFdy(vJgWorldPos)));
|
|
74
|
+
JgSurf jgS = jgSurface(JG_RIBBON_CLASS, diffuseColor.rgb, uJgRough, uJgScale, vJgWorldPos);
|
|
75
|
+
vec3 jgCol = jgS.albedo;
|
|
76
|
+
float jgRough = jgS.rough;
|
|
77
|
+
float jgBumpH = jgS.bump;
|
|
78
|
+
float jgAcross = vJgRibbon.y;
|
|
79
|
+
jgCol *= 1.0 + (jgFbm2(jgWp / 14.0 + vec2(3.7, 61.2)) - 0.5) * 0.24;
|
|
80
|
+
#if defined(JG_RIBBON_PAVED) && !defined(JG_SURFACE_LOW)
|
|
81
|
+
{
|
|
82
|
+
// Weathered pavement: resurfaced patches, exposed aggregate and a partial crack network.
|
|
83
|
+
jgCol *= 1.0 + (jgFbm3o(jgWp / 5.0 + 11.0) - 0.5) * 0.34;
|
|
84
|
+
jgCol *= 1.0 + (jgVNoise(jgWp * 16.0) - 0.5) * 0.26 * jgBand(16.0);
|
|
85
|
+
vec3 jgCell = jgVoronoi(jgWp * 0.32 + 5.1);
|
|
86
|
+
float jgCrack = (1.0 - smoothstep(0.0, 0.03, jgCell.y)) * smoothstep(0.45, 0.6, jgVNoise(jgWp * 0.45 + 3.3)) * jgBand(2.0);
|
|
87
|
+
jgCol *= 1.0 - 0.4 * jgCrack;
|
|
88
|
+
jgRough += 0.05 * jgCrack;
|
|
89
|
+
jgBumpH -= jgCrack * 0.004;
|
|
90
|
+
}
|
|
91
|
+
#endif
|
|
92
|
+
#ifdef JG_RIBBON_WEAR
|
|
93
|
+
{
|
|
94
|
+
// Two wheel tracks a cart-axle apart, wandering together along the ribbon.
|
|
95
|
+
float jgWob = (jgVNoise(vec2(vJgRibbon.x * 0.09, 4.7)) - 0.5) * uJgHalfWidth * 0.35;
|
|
96
|
+
float jgRutD = abs(abs(jgAcross - jgWob) - uJgHalfWidth * 0.45);
|
|
97
|
+
float jgRutW = max(0.15, uJgHalfWidth * 0.14);
|
|
98
|
+
float jgRut = exp(-(jgRutD * jgRutD) / (jgRutW * jgRutW)) * (0.55 + 0.45 * jgVNoise(vec2(vJgRibbon.x * 0.4, 9.1))) * uJgWear;
|
|
99
|
+
#ifdef JG_RIBBON_PAVED
|
|
100
|
+
jgCol *= 1.0 - 0.22 * jgRut;
|
|
101
|
+
jgRough -= 0.16 * jgRut;
|
|
102
|
+
#else
|
|
103
|
+
// Trodden band: packed flat, fewer loose grains and a little dustier, so the track reads against
|
|
104
|
+
// painted dirt of the same hue; then darker ruts and loose soil heaped toward the shoulders.
|
|
105
|
+
float jgTrod = (1.0 - smoothstep(uJgHalfWidth * 0.5, uJgHalfWidth * 0.95, abs(jgAcross))) * uJgWear;
|
|
106
|
+
jgCol = mix(jgCol, diffuseColor.rgb * (0.96 + 0.12 * jgFbm2(jgWp * 0.7 + 2.9)), 0.5 * jgTrod);
|
|
107
|
+
jgCol *= 1.0 + 0.08 * jgTrod;
|
|
108
|
+
jgBumpH *= 1.0 - 0.6 * jgTrod;
|
|
109
|
+
jgCol *= 1.0 - 0.32 * jgRut;
|
|
110
|
+
jgRough -= 0.12 * jgRut;
|
|
111
|
+
jgBumpH -= jgRut * 0.03;
|
|
112
|
+
float jgShoulder = smoothstep(uJgHalfWidth * 0.6, uJgHalfWidth, abs(jgAcross)) * uJgWear;
|
|
113
|
+
jgCol *= 1.0 - 0.16 * jgShoulder;
|
|
114
|
+
jgBumpH += jgShoulder * 0.014;
|
|
115
|
+
#endif
|
|
116
|
+
}
|
|
117
|
+
#endif
|
|
118
|
+
#ifdef JG_RIBBON_EDGE
|
|
119
|
+
{
|
|
120
|
+
float jgEdgeN = (jgFbm2(jgWp * 0.55 + 7.3) - 0.5) * 2.0 + (jgVNoise(jgWp * 2.3 + 1.7) - 0.5) * 0.6;
|
|
121
|
+
float jgEdge = uJgHalfWidth + jgEdgeN * uJgWarp;
|
|
122
|
+
float jgAlpha = 1.0 - smoothstep(jgEdge - uJgFeather * 0.5, jgEdge + uJgFeather * 0.5, abs(jgAcross));
|
|
123
|
+
jgAlpha *= smoothstep(0.0, uJgFeather, min(vJgRibbon.x, vJgRibbon.z) + jgEdgeN * uJgWarp);
|
|
124
|
+
// Taller grains (pebbles, clods) hold on longer, so the edge breaks up instead of fading evenly.
|
|
125
|
+
jgAlpha = clamp(jgAlpha + (jgS.height - 0.4) * 2.4 * jgAlpha * (1.0 - jgAlpha), 0.0, 1.0);
|
|
126
|
+
diffuseColor.a *= jgAlpha;
|
|
127
|
+
jgBumpH *= jgAlpha;
|
|
128
|
+
}
|
|
129
|
+
#endif
|
|
130
|
+
#ifdef USE_COLOR_ALPHA
|
|
131
|
+
{
|
|
132
|
+
// A per-vertex fade interpolated across big triangles draws straight iso-lines; warp it with noise
|
|
133
|
+
// so it breaks up like painted terrain. Fully opaque or fully clear vertices are left alone.
|
|
134
|
+
float jgA = diffuseColor.a;
|
|
135
|
+
float jgMid = min(jgA, 1.0 - jgA);
|
|
136
|
+
float jgFadeN = jgFbm2(jgWp * 0.35 + 21.7) - 0.5 + (jgVNoise(jgWp * 1.7 + 5.3) - 0.5) * 0.4;
|
|
137
|
+
diffuseColor.a = clamp(jgA + jgFadeN * min(1.0, jgMid * 4.0) * 1.1, 0.0, 1.0);
|
|
138
|
+
diffuseColor.a = smoothstep(0.08, 0.92, diffuseColor.a);
|
|
139
|
+
}
|
|
140
|
+
#endif
|
|
141
|
+
diffuseColor.rgb = jgCol;
|
|
142
|
+
`;
|
|
143
|
+
/**
|
|
144
|
+
* A `MeshStandardMaterial` that shades a ground ribbon (path, road, sidewalk) with the terrain's own
|
|
145
|
+
* procedural surface functions, so a dirt track or asphalt strip matches painted ground: the
|
|
146
|
+
* authored colour varied by the surface class's grain, roughness and micro-normal, wheel ruts or tyre
|
|
147
|
+
* lanes, and (with `feather > 0`) a noise-warped edge that fades into the terrain. The geometry needs
|
|
148
|
+
* a {@link RIBBON_FRAME_ATTRIBUTE} attribute from {@link ribbonSurfaceFrame}; build it wider by
|
|
149
|
+
* {@link ribbonEdgeMargin} so the fade straddles the authored edge. The caller disposes the material.
|
|
150
|
+
*/
|
|
151
|
+
export function createSurfaceRibbonMaterial(options) {
|
|
152
|
+
const cls = terrainSurfaceClass(options.surface ?? "road");
|
|
153
|
+
const classIndex = terrainSurfaceClassIndex(cls);
|
|
154
|
+
const low = options.quality === "low";
|
|
155
|
+
const feather = Math.max(0, options.feather ?? 0);
|
|
156
|
+
const edge = feather > 0;
|
|
157
|
+
const wear = clamp(options.wear ?? 0, 0, 1);
|
|
158
|
+
const paved = cls === "road" || cls === "rock";
|
|
159
|
+
const roughness = options.roughness ?? terrainSurfaceRoughness(cls);
|
|
160
|
+
const material = new THREE.MeshStandardMaterial({
|
|
161
|
+
color: options.color,
|
|
162
|
+
roughness,
|
|
163
|
+
metalness: 0,
|
|
164
|
+
side: THREE.DoubleSide,
|
|
165
|
+
transparent: edge,
|
|
166
|
+
vertexColors: options.vertexColors === true,
|
|
167
|
+
...(options.polygonOffset === true ? { polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 } : {}),
|
|
168
|
+
});
|
|
169
|
+
const uniforms = {
|
|
170
|
+
uJgRough: { value: roughness },
|
|
171
|
+
uJgScale: { value: options.scale ?? 1 },
|
|
172
|
+
uJgHalfWidth: { value: Math.max(0.01, options.halfWidth) },
|
|
173
|
+
uJgFeather: { value: Math.max(1e-3, feather) },
|
|
174
|
+
uJgWarp: { value: Math.max(0, options.warp ?? 0) },
|
|
175
|
+
uJgWear: { value: wear },
|
|
176
|
+
};
|
|
177
|
+
material.defines = {
|
|
178
|
+
JG_RIBBON_CLASS: String(classIndex),
|
|
179
|
+
JG_SURFACE_CLASSES: String(1 << classIndex),
|
|
180
|
+
...(low ? { JG_SURFACE_LOW: "" } : {}),
|
|
181
|
+
...(edge ? { JG_RIBBON_EDGE: "" } : {}),
|
|
182
|
+
...(wear > 0 ? { JG_RIBBON_WEAR: "" } : {}),
|
|
183
|
+
...(paved ? { JG_RIBBON_PAVED: "" } : {}),
|
|
184
|
+
};
|
|
185
|
+
material.onBeforeCompile = (shader) => {
|
|
186
|
+
Object.assign(shader.uniforms, uniforms);
|
|
187
|
+
shader.vertexShader = shader.vertexShader
|
|
188
|
+
.replace("#include <common>", `#include <common>
|
|
189
|
+
attribute vec3 ${RIBBON_FRAME_ATTRIBUTE};
|
|
190
|
+
varying vec3 vJgWorldPos;
|
|
191
|
+
varying vec3 vJgRibbon;`)
|
|
192
|
+
.replace("#include <begin_vertex>", `#include <begin_vertex>
|
|
193
|
+
vJgWorldPos = (modelMatrix * vec4(transformed, 1.0)).xyz;
|
|
194
|
+
vJgRibbon = ${RIBBON_FRAME_ATTRIBUTE};`);
|
|
195
|
+
shader.fragmentShader = shader.fragmentShader
|
|
196
|
+
.replace("#include <common>", `#include <common>
|
|
197
|
+
varying vec3 vJgWorldPos;
|
|
198
|
+
varying vec3 vJgRibbon;
|
|
199
|
+
uniform float uJgRough;
|
|
200
|
+
uniform float uJgScale;
|
|
201
|
+
uniform float uJgHalfWidth;
|
|
202
|
+
uniform float uJgFeather;
|
|
203
|
+
uniform float uJgWarp;
|
|
204
|
+
uniform float uJgWear;
|
|
205
|
+
${NOISE_GLSL}
|
|
206
|
+
${SURFACE_FUNCTIONS_GLSL}`)
|
|
207
|
+
.replace("#include <color_fragment>", `#include <color_fragment>\n${RIBBON_FRAGMENT_GLSL}`)
|
|
208
|
+
.replace("#include <roughnessmap_fragment>", `#include <roughnessmap_fragment>
|
|
209
|
+
roughnessFactor = clamp(jgRough, 0.04, 1.0);`)
|
|
210
|
+
.replace("#include <normal_fragment_maps>", `#include <normal_fragment_maps>
|
|
211
|
+
${SURFACE_BUMP_NORMAL_GLSL}`);
|
|
212
|
+
};
|
|
213
|
+
material.customProgramCacheKey = () => `jgengine-surface-ribbon-${classIndex}-${low ? "low" : "full"}-${edge ? "edge" : "hard"}-${wear > 0 ? "wear" : "clean"}`;
|
|
214
|
+
return material;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* {@link createSurfaceRibbonMaterial} for a React mesh: rebuilt only when an option changes and
|
|
218
|
+
* disposed on unmount. Pass `null` to get `null` (the caller falls back to a flat material).
|
|
219
|
+
*/
|
|
220
|
+
export function useSurfaceRibbonMaterial(options) {
|
|
221
|
+
const o = options;
|
|
222
|
+
const material = useMemo(() => (o === null ? null : createSurfaceRibbonMaterial(o)),
|
|
223
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
224
|
+
[o === null, o?.color, o?.halfWidth, o?.surface, o?.feather, o?.warp, o?.wear, o?.roughness, o?.scale, o?.quality, o?.polygonOffset, o?.vertexColors]);
|
|
225
|
+
useEffect(() => () => material?.dispose(), [material]);
|
|
226
|
+
return material;
|
|
227
|
+
}
|