@jgengine/shell 0.22.0 → 0.23.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 +96 -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 +86 -24
- 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/index.d.ts +4 -0
- package/dist/terrain/index.js +4 -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 +233 -37
- package/skills/jgengine-ui/capabilities.md +52 -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 +23 -8
package/dist/terrain/index.js
CHANGED
|
@@ -10,3 +10,7 @@ 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";
|
|
@@ -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
|
+
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
|
+
import type { GraphicsQuality } from "@jgengine/core/settings/settingsModel";
|
|
2
3
|
import type { ResolvedTerrainDetail } from "@jgengine/core/world/terrain";
|
|
4
|
+
import type { TerrainPaintTextures } from "./terrainSurfaces.js";
|
|
3
5
|
/** The built procedural detail terrain material, ready to mount on the ground mesh. */
|
|
4
6
|
export interface TerrainDetailMaterialHandle {
|
|
5
7
|
material: THREE.MeshStandardMaterial;
|
|
8
|
+
/** Editor relief-contour strength 0..1 (0 in play); write `.value` to retune without recompiling. */
|
|
9
|
+
contour: {
|
|
10
|
+
value: number;
|
|
11
|
+
};
|
|
6
12
|
}
|
|
7
13
|
/**
|
|
8
14
|
* Loaded PBR textures matching a resolved `ResolvedTerrainDetailMaterial.maps`' roles. The caller
|
|
@@ -15,17 +21,34 @@ export interface TerrainDetailMaterialTextures {
|
|
|
15
21
|
roughness?: THREE.Texture;
|
|
16
22
|
ao?: THREE.Texture;
|
|
17
23
|
}
|
|
24
|
+
/** Surface-pass inputs beyond the resolved detail config. */
|
|
25
|
+
export interface TerrainDetailMaterialOptions {
|
|
26
|
+
/** Authored paint coverage from {@link createTerrainPaintTextures}; omit for unpainted ground. */
|
|
27
|
+
paint?: TerrainPaintTextures;
|
|
28
|
+
/** Graphics tier. `low` draws one cheap mottle per surface and skips micro-normals. Default `high`. */
|
|
29
|
+
quality?: GraphicsQuality;
|
|
30
|
+
/**
|
|
31
|
+
* Surface classes to compile (bit `1 << classIndex`); defaults to the paint's own classes. An
|
|
32
|
+
* editor that can paint any surface passes {@link ALL_TERRAIN_SURFACE_CLASSES}.
|
|
33
|
+
*/
|
|
34
|
+
classMask?: number;
|
|
35
|
+
/** Initial editor relief-contour strength. Default 0. */
|
|
36
|
+
contour?: number;
|
|
37
|
+
}
|
|
38
|
+
/** Compile every surface response, for an editor viewport whose paint can change to any surface. */
|
|
39
|
+
export declare const ALL_TERRAIN_SURFACE_CLASSES = 511;
|
|
18
40
|
/**
|
|
19
|
-
* A `MeshStandardMaterial`
|
|
20
|
-
*
|
|
21
|
-
* sand
|
|
22
|
-
*
|
|
23
|
-
*
|
|
41
|
+
* A `MeshStandardMaterial` that renders terrain as ground rather than flat colour. Each painted
|
|
42
|
+
* layer draws its surface type's procedural response (grass clumps and blades, dirt clods and
|
|
43
|
+
* pebbles, gravel stones, cracked rock, rippled sand, wet mud, snow, road grain) in the layer's
|
|
44
|
+
* authored hue, with albedo, roughness and a micro-normal. Layers meet along noise-warped,
|
|
45
|
+
* height-aware edges instead of paint-cell blobs; steep slopes turn to rock; macro sweeps break up
|
|
46
|
+
* tiling. Unpainted ground keeps the biome-tinted vertex colour with procedural detail, plus the
|
|
47
|
+
* waterline sand and altitude snow bands. Full PBR: lit, shadowed and fogged like any standard material.
|
|
24
48
|
*
|
|
25
49
|
* When `detail.material` is set, pass the matching loaded `textures` (color/normal/roughness/ao):
|
|
26
50
|
* they tile by world position at `detail.material.repeat` world units per tile and blend over the
|
|
27
|
-
* procedural result at `detail.material.strength
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
export declare function createTerrainDetailMaterial(detail: ResolvedTerrainDetail, textures?: TerrainDetailMaterialTextures): TerrainDetailMaterialHandle;
|
|
51
|
+
* procedural result at `detail.material.strength`.
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
export declare function createTerrainDetailMaterial(detail: ResolvedTerrainDetail, textures?: TerrainDetailMaterialTextures, options?: TerrainDetailMaterialOptions): TerrainDetailMaterialHandle;
|
|
@@ -1,49 +1,58 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
2
|
+
import { NOISE_GLSL, PAINT_PARS_GLSL, SURFACE_BUMP_NORMAL_GLSL, SURFACE_FUNCTIONS_GLSL } from "./terrainSurfaceShader.js";
|
|
3
|
+
const ROCK_CLASS_BIT = 1 << 4;
|
|
4
|
+
/** Compile every surface response, for an editor viewport whose paint can change to any surface. */
|
|
5
|
+
export const ALL_TERRAIN_SURFACE_CLASSES = 0x1ff;
|
|
6
|
+
// Slot 0 is the unpainted base, 1–8 the paint layers, 9 slope rock.
|
|
7
|
+
const SLOT_GLSL = /* glsl */ `
|
|
8
|
+
JgSurf jgSlot(int slot, vec3 baseTone, vec3 wp){
|
|
9
|
+
vec3 jgFlatC = baseTone;
|
|
10
|
+
JgSurf s;
|
|
11
|
+
if (slot == 9) {
|
|
12
|
+
jgFlatC = uRockColor;
|
|
13
|
+
s = jgSurface(4, uRockColor, roughness, 1.0, wp);
|
|
14
|
+
}
|
|
15
|
+
#ifdef JG_TERRAIN_PAINT
|
|
16
|
+
else if (slot > 0) {
|
|
17
|
+
int i = slot - 1;
|
|
18
|
+
jgFlatC = uLayerColor[i];
|
|
19
|
+
s = jgSurface(int(uLayerClass[i] + 0.5), uLayerColor[i], uLayerRough[i], uLayerScale[i], wp);
|
|
20
|
+
}
|
|
21
|
+
#endif
|
|
22
|
+
else {
|
|
23
|
+
s = jgSurface(0, baseTone, roughness, 1.0, wp);
|
|
24
|
+
}
|
|
25
|
+
s.albedo = mix(jgFlatC, s.albedo, uSurface);
|
|
26
|
+
s.height *= uSurface;
|
|
27
|
+
s.bump *= uSurface;
|
|
28
|
+
return s;
|
|
27
29
|
}
|
|
28
30
|
`;
|
|
29
31
|
/**
|
|
30
|
-
* A `MeshStandardMaterial`
|
|
31
|
-
*
|
|
32
|
-
* sand
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* A `MeshStandardMaterial` that renders terrain as ground rather than flat colour. Each painted
|
|
33
|
+
* layer draws its surface type's procedural response (grass clumps and blades, dirt clods and
|
|
34
|
+
* pebbles, gravel stones, cracked rock, rippled sand, wet mud, snow, road grain) in the layer's
|
|
35
|
+
* authored hue, with albedo, roughness and a micro-normal. Layers meet along noise-warped,
|
|
36
|
+
* height-aware edges instead of paint-cell blobs; steep slopes turn to rock; macro sweeps break up
|
|
37
|
+
* tiling. Unpainted ground keeps the biome-tinted vertex colour with procedural detail, plus the
|
|
38
|
+
* waterline sand and altitude snow bands. Full PBR: lit, shadowed and fogged like any standard material.
|
|
35
39
|
*
|
|
36
40
|
* When `detail.material` is set, pass the matching loaded `textures` (color/normal/roughness/ao):
|
|
37
41
|
* they tile by world position at `detail.material.repeat` world units per tile and blend over the
|
|
38
|
-
* procedural result at `detail.material.strength
|
|
39
|
-
*
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
export function createTerrainDetailMaterial(detail, textures) {
|
|
42
|
+
* procedural result at `detail.material.strength`.
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
export function createTerrainDetailMaterial(detail, textures, options = {}) {
|
|
43
46
|
const hasTexture = textures !== undefined && detail.material !== undefined;
|
|
44
47
|
const repeat = detail.material?.repeat ?? 4;
|
|
45
48
|
const strength = detail.material?.strength ?? 1;
|
|
46
49
|
const tint = detail.material?.tint ?? 1;
|
|
50
|
+
const paint = options.paint;
|
|
51
|
+
const low = options.quality === "low";
|
|
52
|
+
const contour = { value: options.contour ?? 0 };
|
|
53
|
+
// Rock is always compiled for slope rock; each extra class costs shader time even where unused on
|
|
54
|
+
// GPUs that run both sides of a branch.
|
|
55
|
+
const classMask = (options.classMask ?? paint?.classMask ?? 0) | ROCK_CLASS_BIT;
|
|
47
56
|
const uniforms = {
|
|
48
57
|
uRockColor: { value: new THREE.Color(detail.rockColor) },
|
|
49
58
|
uSandColor: { value: new THREE.Color(detail.sandColor) },
|
|
@@ -56,6 +65,9 @@ export function createTerrainDetailMaterial(detail, textures) {
|
|
|
56
65
|
uDetailScale: { value: Math.max(0.5, detail.detailScale) },
|
|
57
66
|
uMacroScale: { value: Math.max(0.5, detail.macroScale) },
|
|
58
67
|
uStrength: { value: detail.strength },
|
|
68
|
+
uSurface: { value: detail.surfaceStrength },
|
|
69
|
+
uContour: contour,
|
|
70
|
+
...(paint === undefined ? {} : paint.uniforms),
|
|
59
71
|
...(hasTexture
|
|
60
72
|
? {
|
|
61
73
|
uMatColor: { value: textures.color },
|
|
@@ -74,6 +86,11 @@ export function createTerrainDetailMaterial(detail, textures) {
|
|
|
74
86
|
metalness: 0,
|
|
75
87
|
vertexColors: true,
|
|
76
88
|
});
|
|
89
|
+
material.defines = {
|
|
90
|
+
...(paint === undefined ? {} : { JG_TERRAIN_PAINT: "" }),
|
|
91
|
+
...(low ? { JG_SURFACE_LOW: "" } : {}),
|
|
92
|
+
JG_SURFACE_CLASSES: String(classMask),
|
|
93
|
+
};
|
|
77
94
|
material.onBeforeCompile = (shader) => {
|
|
78
95
|
Object.assign(shader.uniforms, uniforms);
|
|
79
96
|
shader.vertexShader = shader.vertexShader
|
|
@@ -109,12 +126,15 @@ jgCol = mix(jgCol, jgMatColor, uMatStrength * uMatTint);`
|
|
|
109
126
|
const materialRoughnessGlsl = hasTexture && textures.roughness !== undefined
|
|
110
127
|
? `
|
|
111
128
|
float jgMatRough = texture2D(uMatRoughness, jgMatUv).g;
|
|
112
|
-
|
|
129
|
+
// The map varies the authored roughness rather than replacing it: scanned ground roughness sits
|
|
130
|
+
// near 0.45, which turned matte terrain glossy enough to throw a wet sheen across dry sand.
|
|
131
|
+
roughnessFactor = clamp(roughnessFactor + (jgMatRough - 0.5) * uMatStrength * 0.6, 0.0, 1.0);`
|
|
113
132
|
: "";
|
|
133
|
+
// The map's tilt is world-space (U along +X, V along +Z on flat ground); `normal` is view-space.
|
|
114
134
|
const materialNormalGlsl = hasTexture
|
|
115
135
|
? `
|
|
116
136
|
vec2 jgMatNormalXy = texture2D(uMatNormal, jgMatUv).rg * 2.0 - 1.0;
|
|
117
|
-
normal = normalize(normal +
|
|
137
|
+
normal = normalize(normal + (viewMatrix * vec4(jgMatNormalXy.x, 0.0, jgMatNormalXy.y, 0.0)).xyz * uMatStrength * 0.6);`
|
|
118
138
|
: "";
|
|
119
139
|
const materialAoGlsl = hasTexture && textures.ao !== undefined
|
|
120
140
|
? `
|
|
@@ -136,48 +156,114 @@ uniform float uWaterLevel;
|
|
|
136
156
|
uniform float uDetailScale;
|
|
137
157
|
uniform float uMacroScale;
|
|
138
158
|
uniform float uStrength;
|
|
159
|
+
uniform float uSurface;
|
|
160
|
+
uniform float uContour;
|
|
139
161
|
${materialUniformsGlsl}
|
|
140
|
-
${NOISE_GLSL}
|
|
162
|
+
${NOISE_GLSL}
|
|
163
|
+
${SURFACE_FUNCTIONS_GLSL}
|
|
164
|
+
#ifdef JG_TERRAIN_PAINT
|
|
165
|
+
${PAINT_PARS_GLSL}
|
|
166
|
+
#endif
|
|
167
|
+
${SLOT_GLSL}`)
|
|
141
168
|
.replace("#include <color_fragment>", `#include <color_fragment>
|
|
142
169
|
vec2 jgWp = vJgWorldPos.xz;
|
|
143
|
-
|
|
170
|
+
jgFootprint = max(length(dFdx(vJgWorldPos)), length(dFdy(vJgWorldPos)));
|
|
144
171
|
float jgMacro = jgFbm(jgWp / uMacroScale);
|
|
145
|
-
float jgPatch = jgFbm(jgWp / (uMacroScale * 0.35) + vec2(37.2, 11.7));
|
|
146
172
|
float jgSlope = 1.0 - clamp(vJgWorldNormal.y, 0.0, 1.0);
|
|
147
173
|
float jgH = vJgWorldPos.y;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
174
|
+
float jgRockW = smoothstep(uRockSlopeStart, uRockSlopeStart + 0.18, jgSlope) * mix(uStrength, 1.0, uSurface);
|
|
175
|
+
|
|
176
|
+
// Only the two strongest of base, eight paint slots and slope rock are shaded; a third
|
|
177
|
+
// contribution is negligible once the coverage is smoothed, and shading every slot cost 10x.
|
|
178
|
+
vec4 jgPa = vec4(0.0);
|
|
179
|
+
vec4 jgPb = vec4(0.0);
|
|
180
|
+
float jgPaintTotal = 0.0;
|
|
181
|
+
#ifdef JG_TERRAIN_PAINT
|
|
182
|
+
vec2 jgPaintTexUv = jgPaintUv(jgWp);
|
|
183
|
+
jgPa = texture2D(uPaintA, jgPaintTexUv);
|
|
184
|
+
jgPb = texture2D(uPaintB, jgPaintTexUv);
|
|
185
|
+
jgPaintTotal = dot(jgPa, vec4(1.0)) + dot(jgPb, vec4(1.0));
|
|
186
|
+
#endif
|
|
187
|
+
float jgPaintNorm = (1.0 - jgRockW) / max(1.0, jgPaintTotal);
|
|
188
|
+
jgPaintTotal = min(jgPaintTotal, 1.0);
|
|
189
|
+
int jgI1 = 0;
|
|
190
|
+
int jgI2 = 0;
|
|
191
|
+
float jgW1 = (1.0 - jgPaintTotal) * (1.0 - jgRockW);
|
|
192
|
+
float jgW2 = 0.0;
|
|
193
|
+
#define JG_PICK(idx, w) { float jgPk = (w); if (jgPk > jgW1) { jgI2 = jgI1; jgW2 = jgW1; jgI1 = idx; jgW1 = jgPk; } else if (jgPk > jgW2) { jgI2 = idx; jgW2 = jgPk; } }
|
|
194
|
+
#ifdef JG_TERRAIN_PAINT
|
|
195
|
+
JG_PICK(1, jgPa.r * jgPaintNorm)
|
|
196
|
+
JG_PICK(2, jgPa.g * jgPaintNorm)
|
|
197
|
+
JG_PICK(3, jgPa.b * jgPaintNorm)
|
|
198
|
+
JG_PICK(4, jgPa.a * jgPaintNorm)
|
|
199
|
+
JG_PICK(5, jgPb.r * jgPaintNorm)
|
|
200
|
+
JG_PICK(6, jgPb.g * jgPaintNorm)
|
|
201
|
+
JG_PICK(7, jgPb.b * jgPaintNorm)
|
|
202
|
+
JG_PICK(8, jgPb.a * jgPaintNorm)
|
|
203
|
+
#endif
|
|
204
|
+
JG_PICK(9, jgRockW)
|
|
205
|
+
bool jgTwo = jgW2 > 0.004;
|
|
206
|
+
|
|
154
207
|
vec3 jgBase = diffuseColor.rgb;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
jgBase *= 1.0
|
|
161
|
-
|
|
162
|
-
jgBase
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
208
|
+
if (jgI1 == 0 || (jgTwo && jgI2 == 0)) {
|
|
209
|
+
float jgFine = jgFbm(jgWp / uDetailScale);
|
|
210
|
+
float jgPatch = jgFbm(jgWp / (uMacroScale * 0.35) + vec2(37.2, 11.7));
|
|
211
|
+
float jgFleck = jgVNoise(jgWp / max(0.05, uDetailScale * 0.024));
|
|
212
|
+
float jgFleckMid = jgVNoise(jgWp / max(0.12, uDetailScale * 0.07) + vec2(51.3, 7.9));
|
|
213
|
+
jgBase *= mix(1.0 - 0.2 * uStrength, 1.0 + 0.16 * uStrength, jgFine);
|
|
214
|
+
// Biome patchwork: hue-shifted dry sweeps and cooler wet pockets, tinted per-world via detail.sweeps.
|
|
215
|
+
jgBase = mix(jgBase, jgBase * uSweepDry, smoothstep(0.5, 0.8, jgMacro) * 0.55 * uStrength);
|
|
216
|
+
jgBase = mix(jgBase, jgBase * uSweepWet, smoothstep(0.52, 0.85, jgPatch) * 0.5 * uStrength);
|
|
217
|
+
jgBase *= 1.0 + (jgFbm(jgWp / (uMacroScale * 2.3) + vec2(9.1, 73.4)) - 0.5) * 0.22 * uStrength;
|
|
218
|
+
jgBase *= 1.0 + (jgFleck - 0.5) * 0.48 * uStrength * jgBand(1.0 / max(0.05, uDetailScale * 0.024));
|
|
219
|
+
jgBase *= 1.0 + (jgFleckMid - 0.5) * 0.34 * uStrength * jgBand(1.0 / max(0.12, uDetailScale * 0.07));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
JgSurf jgS1 = jgSlot(jgI1, jgBase, vJgWorldPos);
|
|
223
|
+
JgSurf jgS2 = jgS1;
|
|
224
|
+
float jgA1 = 1.0;
|
|
225
|
+
float jgA2 = 0.0;
|
|
226
|
+
if (jgTwo) {
|
|
227
|
+
jgS2 = jgSlot(jgI2, jgBase, vJgWorldPos);
|
|
228
|
+
float jgB1 = jgW1 / (jgW1 + jgW2);
|
|
229
|
+
float jgB2 = 1.0 - jgB1;
|
|
230
|
+
// Height-aware blend: along an edge the layer standing taller (gravel stones, grass clumps) wins
|
|
231
|
+
// first, so transitions read as one material breaking through another, not a cross-fade.
|
|
232
|
+
float jgH1 = jgB1 + jgS1.height * 0.5;
|
|
233
|
+
float jgH2 = jgB2 + jgS2.height * 0.5;
|
|
234
|
+
float jgM = max(jgH1, jgH2) - 0.28;
|
|
235
|
+
jgA1 = mix(jgB1, max(0.0, jgH1 - jgM), uSurface);
|
|
236
|
+
jgA2 = mix(jgB2, max(0.0, jgH2 - jgM), uSurface);
|
|
237
|
+
float jgT = max(jgA1 + jgA2, 1e-4);
|
|
238
|
+
jgA1 /= jgT;
|
|
239
|
+
jgA2 /= jgT;
|
|
240
|
+
}
|
|
241
|
+
vec3 jgCol = jgS1.albedo * jgA1 + jgS2.albedo * jgA2;
|
|
242
|
+
float jgRough = jgS1.rough * jgA1 + jgS2.rough * jgA2;
|
|
243
|
+
float jgBumpH = jgS1.bump * jgA1 + jgS2.bump * jgA2;
|
|
244
|
+
|
|
245
|
+
// Large-scale light and sun-dried sweeps over painted ground, so a field painted one hue never tiles flat.
|
|
246
|
+
float jgPaintCover = jgPaintTotal * (1.0 - jgRockW) * uSurface;
|
|
247
|
+
jgCol *= 1.0 + (jgFbm2(jgWp / (uMacroScale * 1.3) + vec2(3.7, 61.2)) - 0.5) * 0.3 * jgPaintCover;
|
|
248
|
+
jgCol = mix(jgCol, jgCol * uSweepDry, smoothstep(0.52, 0.78, jgMacro) * 0.2 * jgPaintCover);
|
|
249
|
+
|
|
167
250
|
float jgSandW = smoothstep(uWaterLevel + 2.4, uWaterLevel - 0.4, jgH) * (1.0 - jgRockW) * uStrength;
|
|
168
|
-
jgCol = mix(jgCol,
|
|
169
|
-
vec3 jgSnow = uSnowColor * mix(0.94, 1.04, jgFine);
|
|
251
|
+
jgCol = mix(jgCol, uSandColor * (0.9 + 0.2 * jgMacro), jgSandW);
|
|
170
252
|
float jgSnowW = smoothstep(uSnowHeight, uSnowHeight + 8.0, jgH) * (1.0 - jgRockW * 0.6) * uStrength;
|
|
171
|
-
jgCol = mix(jgCol,
|
|
253
|
+
jgCol = mix(jgCol, uSnowColor * (0.94 + 0.1 * jgMacro), jgSnowW);
|
|
172
254
|
${materialColorGlsl}
|
|
255
|
+
float jgContour = 0.5 + 0.5 * cos(jgH / 1.5 * 6.2831853);
|
|
256
|
+
jgCol *= (1.0 - uContour * 0.4 * (1.0 - jgContour)) * (1.0 + clamp(jgH * 0.03, -0.12, 0.22) * uContour);
|
|
173
257
|
diffuseColor.rgb = jgCol;`)
|
|
174
258
|
.replace("#include <roughnessmap_fragment>", `#include <roughnessmap_fragment>
|
|
259
|
+
roughnessFactor = clamp(jgRough, 0.04, 1.0);
|
|
175
260
|
${materialRoughnessGlsl}`)
|
|
176
261
|
.replace("#include <normal_fragment_maps>", `#include <normal_fragment_maps>
|
|
262
|
+
${SURFACE_BUMP_NORMAL_GLSL}
|
|
177
263
|
${materialNormalGlsl}`)
|
|
178
264
|
.replace("#include <aomap_fragment>", `#include <aomap_fragment>
|
|
179
265
|
${materialAoGlsl}`);
|
|
180
266
|
};
|
|
181
|
-
material.customProgramCacheKey = () => `jgengine-terrain-detail-${
|
|
182
|
-
return { material };
|
|
267
|
+
material.customProgramCacheKey = () => `jgengine-terrain-detail-${hasTexture ? "tex" : "flat"}-${hasTexture && textures.roughness !== undefined ? "rough" : "scalar"}-${hasTexture && textures.ao !== undefined ? "ao" : "unoccluded"}-${paint === undefined ? "bare" : "paint"}-${low ? "low" : "full"}-${classMask}`;
|
|
268
|
+
return { material, contour };
|
|
183
269
|
}
|