@jgengine/shell 0.23.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 +14 -0
- package/dist/scene/CityRenderer.js +66 -11
- package/dist/terrain/groundEdgeFade.d.ts +67 -0
- package/dist/terrain/groundEdgeFade.js +234 -0
- package/dist/terrain/index.d.ts +1 -0
- package/dist/terrain/index.js +1 -0
- package/package.json +4 -4
- package/skills/jgengine/api.md +2 -2
- package/skills/jgengine-ui/api.md +21 -0
- package/skills/jgengine-ui/capabilities.md +6 -0
- package/skills/jgengine-world/reference.md +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -26,6 +26,20 @@ At publish, rename this heading to the new version and mirror the entries into
|
|
|
26
26
|
`packages/core/src/meta/changelog.ts` (the typed `CHANGELOG` export).
|
|
27
27
|
-->
|
|
28
28
|
|
|
29
|
+
## 0.24.0
|
|
30
|
+
|
|
31
|
+
### Migrate
|
|
32
|
+
|
|
33
|
+
- **Bump lockstep SDK packages to `^0.24.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.21.0`; `@jgengine/github` is `0.11.0`.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `buildGroundCoverageMask`, `createGroundEdgeFade` and `applyGroundEdgeFade` (`@jgengine/shell/terrain`) fade any ground material out at the outer edge of a merged mesh footprint, leaving seams between layers inside it crisp.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- City studio ground (roads, junctions, block land, sidewalks, medians, drives, parks, crops, curbs, markings) now fades into the terrain where the town's ground ends instead of stopping at a hard edge. `meta.edgeFade: false` on the city volume keeps the old hard edge.
|
|
42
|
+
|
|
29
43
|
## 0.23.0
|
|
30
44
|
|
|
31
45
|
### Migrate
|
|
@@ -14,7 +14,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
14
14
|
* All geometry derives from the document volume, so editor sliders re-render the district live and
|
|
15
15
|
* games consume the same authored volume unchanged.
|
|
16
16
|
*/
|
|
17
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
17
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
18
18
|
import { useFrame } from "@react-three/fiber";
|
|
19
19
|
import * as THREE from "three";
|
|
20
20
|
import { roundPathCorners, buildIntersectionMarkings, buildRoadRibbon, buildTrimmedIntersections, dashSegments, GROUND_DECAL_LAYERS, } from "@jgengine/core/world/roads";
|
|
@@ -30,6 +30,7 @@ import { InstancedBuildings } from "../structures/GeneratedBuilding.js";
|
|
|
30
30
|
import { getBuildingKit } from "../structures/buildingKitRegistry.js";
|
|
31
31
|
import { createCityWindowMaterial } from "./cityWindowMaterial.js";
|
|
32
32
|
import { createSurfaceRibbonMaterial } from "../terrain/surfaceRibbonMaterial.js";
|
|
33
|
+
import { applyGroundEdgeFade, buildGroundCoverageMask, createGroundEdgeFade } from "../terrain/groundEdgeFade.js";
|
|
33
34
|
import { useTerrainQuality } from "../terrain/terrainQuality.js";
|
|
34
35
|
import { registerSceneKindRenderer } from "./sceneKindRenderers.js";
|
|
35
36
|
import { junctionSurface } from "./cityJunctionSurface.js";
|
|
@@ -91,6 +92,22 @@ const CROP_LAYER = 0.42;
|
|
|
91
92
|
const RACE_SURFACE_LAYER = 0.28;
|
|
92
93
|
const RACE_PAINT_LAYER = 0.3;
|
|
93
94
|
const BRIDGE_DECK_LIFT = 0.12;
|
|
95
|
+
/** Ground meshes bottom layer first; the index is each mesh's `renderOrder`, so blended layers draw under the ones above. */
|
|
96
|
+
const GROUND_LAYERS = [
|
|
97
|
+
"urban",
|
|
98
|
+
"asphalt",
|
|
99
|
+
"gravel",
|
|
100
|
+
"patches",
|
|
101
|
+
"curbs",
|
|
102
|
+
"markings",
|
|
103
|
+
"land",
|
|
104
|
+
"medians",
|
|
105
|
+
"sidewalks",
|
|
106
|
+
"parks",
|
|
107
|
+
"pavementDrives",
|
|
108
|
+
"gravelDrives",
|
|
109
|
+
"crops",
|
|
110
|
+
];
|
|
94
111
|
/** Compact carriageway-union — same seam as StreetGeometryPreview and playground `cityScene`. */
|
|
95
112
|
const JUNCTION_OPTS = { curbReturnRadius: 2, apronMargin: 0.25, filletSegments: 6, elevation: ROAD_LAYER, maxSegmentLength: 2 };
|
|
96
113
|
const SPECIES_PROXY = { broadleaf: "oak", conifer: "pine", palm: "palm", cypress: "cypress" };
|
|
@@ -1506,34 +1523,72 @@ function OneCity({ object, context }) {
|
|
|
1506
1523
|
}, [race]);
|
|
1507
1524
|
const quality = useTerrainQuality();
|
|
1508
1525
|
const surfaceDetail = object.meta?.["surfaceDetail"] !== false;
|
|
1509
|
-
|
|
1510
|
-
|
|
1526
|
+
// Every opaque ground layer fades out where the district's merged ground footprint meets bare
|
|
1527
|
+
// terrain; seams between layers inside it stay crisp. The urban sheet is left out of the
|
|
1528
|
+
// footprint because it already fades over its own street-distance reach.
|
|
1529
|
+
const edgeFadeOn = object.meta?.["edgeFade"] !== false;
|
|
1530
|
+
const edgeFade = useMemo(() => (edgeFadeOn ? createGroundEdgeFade() : null), [edgeFadeOn]);
|
|
1531
|
+
useEffect(() => () => edgeFade?.dispose(), [edgeFade]);
|
|
1532
|
+
const fadeWidth = Math.min(4, Math.max(1.5, (resolved?.rules.streetWidth ?? 3) * 0.6));
|
|
1533
|
+
const footprint = useMemo(() => {
|
|
1534
|
+
if (edgeFade === null || ground === null)
|
|
1511
1535
|
return null;
|
|
1512
|
-
const
|
|
1536
|
+
const layers = GROUND_LAYERS.filter((layer) => layer !== "urban" && layer !== "curbs" && layer !== "markings").map((layer) => ground[layer]);
|
|
1537
|
+
return buildGroundCoverageMask(layers, { width: fadeWidth });
|
|
1538
|
+
}, [edgeFade, ground, fadeWidth]);
|
|
1539
|
+
useLayoutEffect(() => {
|
|
1540
|
+
edgeFade?.setMask(footprint, fadeWidth);
|
|
1541
|
+
}, [edgeFade, footprint, fadeWidth]);
|
|
1542
|
+
const groundMaterials = useMemo(() => {
|
|
1543
|
+
const make = (surface, color, vertexColors, roughness, scale = 1) => surfaceDetail
|
|
1544
|
+
? createSurfaceRibbonMaterial({ surface, color, vertexColors, roughness, scale, halfWidth: 1, quality })
|
|
1545
|
+
: new THREE.MeshStandardMaterial({ color, vertexColors, roughness, metalness: 0, side: THREE.DoubleSide });
|
|
1513
1546
|
const urban = make("dirt", "#ffffff", true, 1);
|
|
1514
1547
|
urban.transparent = true;
|
|
1515
1548
|
urban.depthWrite = false;
|
|
1516
|
-
|
|
1549
|
+
const materials = {
|
|
1517
1550
|
urban,
|
|
1518
|
-
land: make("dirt", "#ffffff", true, 1),
|
|
1519
1551
|
asphalt: make("road", ASPHALT_COLOR, true, 0.96),
|
|
1520
1552
|
gravel: make("gravel", GRAVEL_COLOR, false, 1),
|
|
1521
|
-
sidewalks: make("pavement", SIDEWALK_COLOR, false, 1, 1.6),
|
|
1522
1553
|
patches: make("road", ASPHALT_COLOR, true, 0.96),
|
|
1554
|
+
curbs: new THREE.MeshStandardMaterial({ color: CURB_COLOR, roughness: 0.9, metalness: 0, side: THREE.DoubleSide }),
|
|
1555
|
+
markings: new THREE.MeshStandardMaterial({
|
|
1556
|
+
color: MARKING_COLOR,
|
|
1557
|
+
vertexColors: true,
|
|
1558
|
+
roughness: 0.8,
|
|
1559
|
+
metalness: 0,
|
|
1560
|
+
side: THREE.DoubleSide,
|
|
1561
|
+
polygonOffset: true,
|
|
1562
|
+
polygonOffsetFactor: -1,
|
|
1563
|
+
polygonOffsetUnits: -1,
|
|
1564
|
+
}),
|
|
1565
|
+
land: make("dirt", "#ffffff", true, 1),
|
|
1523
1566
|
medians: make("grass", MEDIAN_COLOR, false, 1),
|
|
1567
|
+
sidewalks: make("pavement", SIDEWALK_COLOR, false, 1, 1.6),
|
|
1568
|
+
parks: make("ground", "#ffffff", true, 1),
|
|
1524
1569
|
pavementDrives: make("concrete", PAVEMENT_COLOR, false, 1, 1.6),
|
|
1525
1570
|
gravelDrives: make("gravel", GRAVEL_COLOR, false, 1),
|
|
1526
|
-
parks: make("ground", "#ffffff", true, 1),
|
|
1527
1571
|
crops: make("dirt", "#ffffff", true, 1),
|
|
1528
1572
|
};
|
|
1529
|
-
|
|
1573
|
+
if (edgeFade !== null) {
|
|
1574
|
+
for (const layer of GROUND_LAYERS)
|
|
1575
|
+
if (layer !== "urban")
|
|
1576
|
+
applyGroundEdgeFade(materials[layer], edgeFade);
|
|
1577
|
+
}
|
|
1578
|
+
return materials;
|
|
1579
|
+
}, [surfaceDetail, quality, edgeFade]);
|
|
1530
1580
|
useEffect(() => () => {
|
|
1531
|
-
for (const material of Object.values(groundMaterials
|
|
1581
|
+
for (const material of Object.values(groundMaterials))
|
|
1532
1582
|
material.dispose();
|
|
1533
1583
|
}, [groundMaterials]);
|
|
1534
1584
|
if (resolved === null)
|
|
1535
1585
|
return null;
|
|
1536
|
-
return (_jsxs("group", { children: [
|
|
1586
|
+
return (_jsxs("group", { children: [GROUND_LAYERS.map((layer, order) => {
|
|
1587
|
+
const geometry = ground?.[layer];
|
|
1588
|
+
if (geometry == null)
|
|
1589
|
+
return null;
|
|
1590
|
+
return (_jsx("mesh", { geometry: geometry, material: groundMaterials[layer], renderOrder: order, receiveShadow: layer !== "markings", castShadow: layer === "curbs" }, layer));
|
|
1591
|
+
}), bridges?.deck != null ? (_jsx("mesh", { geometry: bridges.deck, castShadow: true, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: CONCRETE_COLOR, roughness: 0.85, metalness: 0.05, side: THREE.DoubleSide }) })) : null, bridges !== null && bridges.concrete.length > 0 ? (_jsx(InstancedBoxes, { matrices: bridges.concrete, color: CONCRETE_COLOR })) : null, bridges !== null && bridges.steel.length > 0 ? _jsx(InstancedBoxes, { matrices: bridges.steel, color: STEEL_COLOR }) : null, tunnels?.roof != null ? (_jsx("mesh", { geometry: tunnels.roof, castShadow: true, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: TUNNEL_COLOR, roughness: 0.95, metalness: 0, side: THREE.DoubleSide }) })) : null, tunnels !== null && tunnels.stone.length > 0 ? _jsx(InstancedBoxes, { matrices: tunnels.stone, color: TUNNEL_COLOR }) : null, massing !== null ? massing.meshes.map((mesh, i) => _jsx("primitive", { object: mesh }, `massing:${i}`)) : null, furniture !== null ? furniture.meshes.map((mesh, i) => _jsx("primitive", { object: mesh }, `furniture:${i}`)) : null, race !== null ? race.meshes.map((mesh, i) => _jsx("primitive", { object: mesh }, `race:${i}`)) : null, _jsx(DetailBuildings, { buildings: detail.buildings, palette: palette, kitName: resolved?.rules.buildingKit ?? "" })] }));
|
|
1537
1592
|
}
|
|
1538
1593
|
/** One instanced unit-box mesh from precomposed matrices (bridge parts). */
|
|
1539
1594
|
function InstancedBoxes({ matrices, color }) {
|
|
@@ -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
|
@@ -14,3 +14,4 @@ export { ALL_TERRAIN_SURFACE_CLASSES, createTerrainDetailMaterial, type TerrainD
|
|
|
14
14
|
export { createTerrainPaintTextures, MAX_TERRAIN_PAINT_LAYERS, packTerrainPaintWeights, terrainSurfaceClass, terrainSurfaceClassIndex, terrainSurfaceRoughness, type TerrainPaintSlot, type TerrainPaintTextures, type TerrainPaintWeights, type TerrainSurfaceClass, } from "./terrainSurfaces.js";
|
|
15
15
|
export { TerrainQualityContext, useTerrainQuality } from "./terrainQuality.js";
|
|
16
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
|
@@ -14,3 +14,4 @@ export { ALL_TERRAIN_SURFACE_CLASSES, createTerrainDetailMaterial, } from "./ter
|
|
|
14
14
|
export { createTerrainPaintTextures, MAX_TERRAIN_PAINT_LAYERS, packTerrainPaintWeights, terrainSurfaceClass, terrainSurfaceClassIndex, terrainSurfaceRoughness, } from "./terrainSurfaces.js";
|
|
15
15
|
export { TerrainQualityContext, useTerrainQuality } from "./terrainQuality.js";
|
|
16
16
|
export { createSurfaceRibbonMaterial, resolvePathSurface, RIBBON_FRAME_ATTRIBUTE, ribbonEdgeMargin, ribbonSurfaceFrame, useSurfaceRibbonMaterial, } from "./surfaceRibbonMaterial.js";
|
|
17
|
+
export { applyGroundEdgeFade, buildGroundCoverageMask, createGroundEdgeFade, } from "./groundEdgeFade.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Game player shell for JGengine: React Three Fiber canvas, orbit camera, input tracking, HUD mounting, GameUiPreview, and a demo game. Consumers supply a GameRegistry.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"test": "bun test src"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@jgengine/core": "^0.
|
|
39
|
-
"@jgengine/react": "^0.
|
|
40
|
-
"@jgengine/ws": "^0.
|
|
38
|
+
"@jgengine/core": "^0.24.0",
|
|
39
|
+
"@jgengine/react": "^0.24.0",
|
|
40
|
+
"@jgengine/ws": "^0.24.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@react-three/drei": "^10.0.0",
|
package/skills/jgengine/api.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
8
8
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
9
|
-
- `VERSION` (const): const VERSION: "0.
|
|
9
|
+
- `VERSION` (const): const VERSION: "0.24.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
10
10
|
|
|
11
11
|
## @jgengine/core/authoring
|
|
12
12
|
|
|
@@ -232,7 +232,7 @@
|
|
|
232
232
|
|
|
233
233
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
234
234
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
235
|
-
- `VERSION` (const): const VERSION: "0.
|
|
235
|
+
- `VERSION` (const): const VERSION: "0.24.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
236
236
|
|
|
237
237
|
## @jgengine/core/runtime/adapter
|
|
238
238
|
|
|
@@ -2828,6 +2828,9 @@
|
|
|
2828
2828
|
- `GrassRange` (type): type GrassRange = number | readonly [min: number, max: number] — ⚠ undocumented
|
|
2829
2829
|
- `GrassShaderUniforms` (interface): interface GrassShaderUniforms { uTime: THREE.IUniform<number>; uWindDirection: THREE.IUniform<THREE.Vector2>; uWindStrength: THREE.IUniform<number>; uWindSpeed: THREE.IUniform<number>; uWindGustScale: THREE.IUniform<number>; uWindFlutter: THREE.IUniform<number>; uColorB… — ⚠ undocumented
|
|
2830
2830
|
- `GrassWindOptions` (interface): interface GrassWindOptions { direction?: readonly [x: number, z: number]; strength?: number; speed?: number; gustScale?: number; flutter?: number; layered?: boolean } — ⚠ undocumented
|
|
2831
|
+
- `GroundCoverageMask` (interface): interface GroundCoverageMask — How far each point of a merged ground footprint lies inside its outer edge, as a grid over world XZ: `data[row * columns + column]` is 0 on or outside the edge and 255 at `width` metres or more inside. Cell centres sit at `origin + (index + 0.5) * cell`.
|
|
2832
|
+
- `GroundCoverageOptions` (interface): interface GroundCoverageOptions — Options for {@link buildGroundCoverageMask}.
|
|
2833
|
+
- `GroundEdgeFade` (interface): interface GroundEdgeFade — Shared uniforms for {@link applyGroundEdgeFade}; `setMask` retargets every material using them without a recompile.
|
|
2831
2834
|
- `HeightfieldColorFn` (type): type HeightfieldColorFn = (x: number, z: number, height: number, out: THREE.Color) => void — Per-vertex color hook for {@link displaceHeightfieldGeometry}; write the tone into `out`.
|
|
2832
2835
|
- `HeightfieldDisplaceOptions` (interface): interface HeightfieldDisplaceOptions — Options for {@link displaceHeightfieldGeometry}.
|
|
2833
2836
|
- `HeightfieldRect` (interface): interface HeightfieldRect — XZ rectangle in world units — matches the shape of a terraform snapshot's `bounds`.
|
|
@@ -2861,6 +2864,9 @@
|
|
|
2861
2864
|
- `TerrainSurfaceClass` (type): type TerrainSurfaceClass = "ground" | "grass" | "dirt" | "gravel" | "rock" | "sand" | "mud" | "snow" | "road" — Procedural ground response the terrain shader renders for a painted surface id.
|
|
2862
2865
|
- `TerrainSurfaceColorOptions` (interface): interface TerrainSurfaceColorOptions — Slope/noise surface shading laid over the height lerp so untextured ground reads like terrain instead of a smooth two-tone gradient. All knobs are optional with sane defaults; every field is a pure function of vertex position (+ `seed`), so the coloring is stable across rebuilds and tests.
|
|
2863
2866
|
- `TerrainVertexColorOptions` (interface): interface TerrainVertexColorOptions { low?: THREE.ColorRepresentation; high?: THREE.ColorRepresentation; waterline?: THREE.ColorRepresentation; waterlineHeight?: number } — ⚠ undocumented
|
|
2867
|
+
- `applyGroundEdgeFade` (function): function applyGroundEdgeFade<M extends THREE.Material>(material: M, fade: GroundEdgeFade): M — Fade a ground material out at the outer edge of a {@link GroundCoverageMask}: clear on the edge, opaque `width` metres inside, with a noise-broken line. Seams between layers inside the footprint are untouched, so a district keeps crisp road/plaza edges while its outer boundary melts into the terrain. Turns on blending (depth writes stay on); give stacked layers ascending `renderOrder` so lower ones draw first. Chains any existing `onBeforeCompile`, so it composes with `createSurfaceRibbonMaterial` and plain `MeshStandardMaterial`s alike.
|
|
2868
|
+
- `buildGroundCoverageMask` (function): function buildGroundCoverageMask(geometries: readonly (THREE.BufferGeometry | null | undefined)[], options: GroundCoverageOptions): GroundCoverageMask | null — Rasterize the XZ footprint of ground meshes (roads, block land, parks…) into a {@link GroundCoverageMask}. Small gaps between layers are closed first, so only the merged footprint's outer edge fades: seams between layers inside it keep their full opacity. Vertical faces (curbs) add nothing. Returns `null` when the meshes cover nothing.
|
|
2869
|
+
- `createGroundEdgeFade` (function): function createGroundEdgeFade(): GroundEdgeFade — Uniforms for one faded footprint, shared by every ground material of one district. Starts fully opaque.
|
|
2864
2870
|
- `createSurfaceRibbonMaterial` (function): function createSurfaceRibbonMaterial(options: SurfaceRibbonMaterialOptions): THREE.MeshStandardMaterial — A `MeshStandardMaterial` that shades a ground ribbon (path, road, sidewalk) with the terrain's own procedural surface functions, so a dirt track or asphalt strip matches painted ground: the authored colour varied by the surface class's grain, roughness and micro-normal, wheel ruts or tyre lanes, and (with `feather > 0`) a noise-warped edge that fades into the terrain. The geometry needs a {@link RIBBON_FRAME_ATTRIBUTE} attribute from {@link ribbonSurfaceFrame}; build it wider by {@link ribbonEdgeMargin} so the fade straddles the authored edge. The caller disposes the material.
|
|
2865
2871
|
- `createTerrainPaintColorSampler` (function): function createTerrainPaintColorSampler(terrain: TerrainPaintSource): TerrainPaintColorSampler — Shares layer tint, opacity and linear weighted-color blending between editor and runtime ground. Paint lives on a coarse cell grid; when the source exposes that grid, each sample smoothly blends the four surrounding cell centres (unpainted cells contribute the caller's base color in `out`), so painted paths and clearings get soft edges instead of grid-aligned stair steps.
|
|
2866
2872
|
- `createTerrainPaintTextures` (function): function createTerrainPaintTextures(weights: TerrainPaintWeights): TerrainPaintTextures — Uploads packed paint coverage ({@link packTerrainPaintWeights}) as two linear-filtered RGBA textures plus per-slot hue, surface class, roughness and pattern scale for the terrain surface material.
|
|
@@ -2893,6 +2899,9 @@
|
|
|
2893
2899
|
- `GrassRange` (type): type GrassRange = number | readonly [min: number, max: number] — ⚠ undocumented
|
|
2894
2900
|
- `GrassShaderUniforms` (interface): interface GrassShaderUniforms { uTime: THREE.IUniform<number>; uWindDirection: THREE.IUniform<THREE.Vector2>; uWindStrength: THREE.IUniform<number>; uWindSpeed: THREE.IUniform<number>; uWindGustScale: THREE.IUniform<number>; uWindFlutter: THREE.IUniform<number>; uColorB… — ⚠ undocumented
|
|
2895
2901
|
- `GrassWindOptions` (interface): interface GrassWindOptions { direction?: readonly [x: number, z: number]; strength?: number; speed?: number; gustScale?: number; flutter?: number; layered?: boolean } — ⚠ undocumented
|
|
2902
|
+
- `GroundCoverageMask` (interface): interface GroundCoverageMask — How far each point of a merged ground footprint lies inside its outer edge, as a grid over world XZ: `data[row * columns + column]` is 0 on or outside the edge and 255 at `width` metres or more inside. Cell centres sit at `origin + (index + 0.5) * cell`.
|
|
2903
|
+
- `GroundCoverageOptions` (interface): interface GroundCoverageOptions — Options for {@link buildGroundCoverageMask}.
|
|
2904
|
+
- `GroundEdgeFade` (interface): interface GroundEdgeFade — Shared uniforms for {@link applyGroundEdgeFade}; `setMask` retargets every material using them without a recompile.
|
|
2896
2905
|
- `HeightfieldColorFn` (type): type HeightfieldColorFn = (x: number, z: number, height: number, out: THREE.Color) => void — Per-vertex color hook for {@link displaceHeightfieldGeometry}; write the tone into `out`.
|
|
2897
2906
|
- `HeightfieldDisplaceOptions` (interface): interface HeightfieldDisplaceOptions — Options for {@link displaceHeightfieldGeometry}.
|
|
2898
2907
|
- `HeightfieldRect` (interface): interface HeightfieldRect — XZ rectangle in world units — matches the shape of a terraform snapshot's `bounds`.
|
|
@@ -2926,6 +2935,9 @@
|
|
|
2926
2935
|
- `TerrainSurfaceClass` (type): type TerrainSurfaceClass = "ground" | "grass" | "dirt" | "gravel" | "rock" | "sand" | "mud" | "snow" | "road" — Procedural ground response the terrain shader renders for a painted surface id.
|
|
2927
2936
|
- `TerrainSurfaceColorOptions` (interface): interface TerrainSurfaceColorOptions — Slope/noise surface shading laid over the height lerp so untextured ground reads like terrain instead of a smooth two-tone gradient. All knobs are optional with sane defaults; every field is a pure function of vertex position (+ `seed`), so the coloring is stable across rebuilds and tests.
|
|
2928
2937
|
- `TerrainVertexColorOptions` (interface): interface TerrainVertexColorOptions { low?: THREE.ColorRepresentation; high?: THREE.ColorRepresentation; waterline?: THREE.ColorRepresentation; waterlineHeight?: number } — ⚠ undocumented
|
|
2938
|
+
- `applyGroundEdgeFade` (function): function applyGroundEdgeFade<M extends THREE.Material>(material: M, fade: GroundEdgeFade): M — Fade a ground material out at the outer edge of a {@link GroundCoverageMask}: clear on the edge, opaque `width` metres inside, with a noise-broken line. Seams between layers inside the footprint are untouched, so a district keeps crisp road/plaza edges while its outer boundary melts into the terrain. Turns on blending (depth writes stay on); give stacked layers ascending `renderOrder` so lower ones draw first. Chains any existing `onBeforeCompile`, so it composes with `createSurfaceRibbonMaterial` and plain `MeshStandardMaterial`s alike.
|
|
2939
|
+
- `buildGroundCoverageMask` (function): function buildGroundCoverageMask(geometries: readonly (THREE.BufferGeometry | null | undefined)[], options: GroundCoverageOptions): GroundCoverageMask | null — Rasterize the XZ footprint of ground meshes (roads, block land, parks…) into a {@link GroundCoverageMask}. Small gaps between layers are closed first, so only the merged footprint's outer edge fades: seams between layers inside it keep their full opacity. Vertical faces (curbs) add nothing. Returns `null` when the meshes cover nothing.
|
|
2940
|
+
- `createGroundEdgeFade` (function): function createGroundEdgeFade(): GroundEdgeFade — Uniforms for one faded footprint, shared by every ground material of one district. Starts fully opaque.
|
|
2929
2941
|
- `createSurfaceRibbonMaterial` (function): function createSurfaceRibbonMaterial(options: SurfaceRibbonMaterialOptions): THREE.MeshStandardMaterial — A `MeshStandardMaterial` that shades a ground ribbon (path, road, sidewalk) with the terrain's own procedural surface functions, so a dirt track or asphalt strip matches painted ground: the authored colour varied by the surface class's grain, roughness and micro-normal, wheel ruts or tyre lanes, and (with `feather > 0`) a noise-warped edge that fades into the terrain. The geometry needs a {@link RIBBON_FRAME_ATTRIBUTE} attribute from {@link ribbonSurfaceFrame}; build it wider by {@link ribbonEdgeMargin} so the fade straddles the authored edge. The caller disposes the material.
|
|
2930
2942
|
- `createTerrainPaintColorSampler` (function): function createTerrainPaintColorSampler(terrain: TerrainPaintSource): TerrainPaintColorSampler — Shares layer tint, opacity and linear weighted-color blending between editor and runtime ground. Paint lives on a coarse cell grid; when the source exposes that grid, each sample smoothly blends the four surrounding cell centres (unpainted cells contribute the caller's base color in `out`), so painted paths and clearings get soft edges instead of grid-aligned stair steps.
|
|
2931
2943
|
- `createTerrainPaintTextures` (function): function createTerrainPaintTextures(weights: TerrainPaintWeights): TerrainPaintTextures — Uploads packed paint coverage ({@link packTerrainPaintWeights}) as two linear-filtered RGBA textures plus per-slot hue, surface class, roughness and pattern scale for the terrain surface material.
|
|
@@ -2991,6 +3003,15 @@
|
|
|
2991
3003
|
- `GrassShaderUniforms` (interface): interface GrassShaderUniforms { uTime: THREE.IUniform<number>; uWindDirection: THREE.IUniform<THREE.Vector2>; uWindStrength: THREE.IUniform<number>; uWindSpeed: THREE.IUniform<number>; uWindGustScale: THREE.IUniform<number>; uWindFlutter: THREE.IUniform<number>; uColorB… — ⚠ undocumented
|
|
2992
3004
|
- `GrassWindOptions` (interface): interface GrassWindOptions { direction?: readonly [x: number, z: number]; strength?: number; speed?: number; gustScale?: number; flutter?: number; layered?: boolean } — ⚠ undocumented
|
|
2993
3005
|
|
|
3006
|
+
## @jgengine/shell/terrain/groundEdgeFade
|
|
3007
|
+
|
|
3008
|
+
- `GroundCoverageMask` (interface): interface GroundCoverageMask — How far each point of a merged ground footprint lies inside its outer edge, as a grid over world XZ: `data[row * columns + column]` is 0 on or outside the edge and 255 at `width` metres or more inside. Cell centres sit at `origin + (index + 0.5) * cell`.
|
|
3009
|
+
- `GroundCoverageOptions` (interface): interface GroundCoverageOptions — Options for {@link buildGroundCoverageMask}.
|
|
3010
|
+
- `GroundEdgeFade` (interface): interface GroundEdgeFade — Shared uniforms for {@link applyGroundEdgeFade}; `setMask` retargets every material using them without a recompile.
|
|
3011
|
+
- `applyGroundEdgeFade` (function): function applyGroundEdgeFade<M extends THREE.Material>(material: M, fade: GroundEdgeFade): M — Fade a ground material out at the outer edge of a {@link GroundCoverageMask}: clear on the edge, opaque `width` metres inside, with a noise-broken line. Seams between layers inside the footprint are untouched, so a district keeps crisp road/plaza edges while its outer boundary melts into the terrain. Turns on blending (depth writes stay on); give stacked layers ascending `renderOrder` so lower ones draw first. Chains any existing `onBeforeCompile`, so it composes with `createSurfaceRibbonMaterial` and plain `MeshStandardMaterial`s alike.
|
|
3012
|
+
- `buildGroundCoverageMask` (function): function buildGroundCoverageMask(geometries: readonly (THREE.BufferGeometry | null | undefined)[], options: GroundCoverageOptions): GroundCoverageMask | null — Rasterize the XZ footprint of ground meshes (roads, block land, parks…) into a {@link GroundCoverageMask}. Small gaps between layers are closed first, so only the merged footprint's outer edge fades: seams between layers inside it keep their full opacity. Vertical faces (curbs) add nothing. Returns `null` when the meshes cover nothing.
|
|
3013
|
+
- `createGroundEdgeFade` (function): function createGroundEdgeFade(): GroundEdgeFade — Uniforms for one faded footprint, shared by every ground material of one district. Starts fully opaque.
|
|
3014
|
+
|
|
2994
3015
|
## @jgengine/shell/terrain/heightfieldGeometry
|
|
2995
3016
|
|
|
2996
3017
|
- `HeightfieldColorFn` (type): type HeightfieldColorFn = (x: number, z: number, height: number, out: THREE.Color) => void — Per-vertex color hook for {@link displaceHeightfieldGeometry}; write the tone into `out`.
|
|
@@ -266,6 +266,12 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
266
266
|
|
|
267
267
|
- `GameContextBridge` (function) · `import { GameContextBridge } from "@jgengine/react"`
|
|
268
268
|
|
|
269
|
+
## ground-edge-fade — fade merged ground layers into the terrain at their outer edge, keeping inner seams crisp
|
|
270
|
+
|
|
271
|
+
- `applyGroundEdgeFade` (function) · `import { applyGroundEdgeFade } from "@jgengine/shell/terrain"`
|
|
272
|
+
- `buildGroundCoverageMask` (function) · `import { buildGroundCoverageMask } from "@jgengine/shell/terrain"`
|
|
273
|
+
- `createGroundEdgeFade` (function) · `import { createGroundEdgeFade } from "@jgengine/shell/terrain"`
|
|
274
|
+
|
|
269
275
|
## heavy-models — models over the per-model triangle budget, for perf probes and smoke assertions
|
|
270
276
|
|
|
271
277
|
- `heavyModels` (function) · `import { heavyModels } from "@jgengine/shell/render/modelLoad"`
|
|
@@ -48,8 +48,12 @@ noise-warped edge that fades into the terrain around the authored width; pavemen
|
|
|
48
48
|
edge with tyre lanes and cracks. Tune with `meta.edgeSoftness` (0 = hard) and `meta.wear` (0..1);
|
|
49
49
|
`meta.surfaceDetail: false` or `<AuthoredScene pathSurfaceDetail={false}>` keeps flat ribbons.
|
|
50
50
|
`road({ surface })` picks the carriageway surface (default `"road"`, `false` for flat colour), and
|
|
51
|
-
city studio ground takes `meta.surfaceDetail: false` on its volume.
|
|
52
|
-
|
|
51
|
+
city studio ground takes `meta.surfaceDetail: false` on its volume. City ground layers fade out
|
|
52
|
+
where the district's merged footprint meets bare terrain, keeping seams between layers crisp;
|
|
53
|
+
`meta.edgeFade: false` on the volume keeps hard outer edges. Custom ribbons use
|
|
54
|
+
`createSurfaceRibbonMaterial` with `ribbonSurfaceFrame` from `@jgengine/shell/terrain`; custom
|
|
55
|
+
ground meshes get the same outer-edge fade from `buildGroundCoverageMask`, `createGroundEdgeFade`
|
|
56
|
+
and `applyGroundEdgeFade`.
|
|
53
57
|
|
|
54
58
|
Custom ground consumers can use `createTerrainPaintColorSampler` from `@jgengine/shell/terrain`
|
|
55
59
|
with a `TerrainPaintSource` (an editable terrain's layer/surface/weight channels). The returned
|