@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
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
const ORIGINAL_KEY = "jgSurfaceOriginal";
|
|
3
|
+
const OPT_OUT_KEY = "jgSurfaceDetail";
|
|
4
|
+
let activeLevel = "full";
|
|
5
|
+
let gameEnabled = true;
|
|
6
|
+
const compiled = new Set();
|
|
7
|
+
const strengthUniform = { value: 1 };
|
|
8
|
+
const VERTEX_PARS = /* glsl */ `
|
|
9
|
+
varying vec3 vJgSurfPos;`;
|
|
10
|
+
// Captured before skinning so the pattern rides the bind pose instead of swimming through animated limbs;
|
|
11
|
+
// the scale terms convert bind/object units to roughly world metres so one frequency suits every pack.
|
|
12
|
+
const VERTEX_MAIN = /* glsl */ `
|
|
13
|
+
vec3 jgSurfScale = vec3(length(modelMatrix[0].xyz), length(modelMatrix[1].xyz), length(modelMatrix[2].xyz));
|
|
14
|
+
#ifdef USE_SKINNING
|
|
15
|
+
jgSurfScale *= length((bindMatrixInverse * boneMatX * bindMatrix)[0].xyz);
|
|
16
|
+
#endif
|
|
17
|
+
vJgSurfPos = transformed * jgSurfScale;
|
|
18
|
+
#ifdef USE_INSTANCING
|
|
19
|
+
vJgSurfPos *= vec3(length(instanceMatrix[0].xyz), length(instanceMatrix[1].xyz), length(instanceMatrix[2].xyz));
|
|
20
|
+
vJgSurfPos += mod(instanceMatrix[3].xyz, 97.0);
|
|
21
|
+
#endif
|
|
22
|
+
#include <skinning_vertex>`;
|
|
23
|
+
const FRAGMENT_PARS = /* glsl */ `
|
|
24
|
+
varying vec3 vJgSurfPos;
|
|
25
|
+
uniform float jgSurfStrength;
|
|
26
|
+
float jgSurfHash(vec3 p) {
|
|
27
|
+
p = fract(p * 0.1031);
|
|
28
|
+
p += dot(p, p.zyx + 31.32);
|
|
29
|
+
return fract((p.x + p.y) * p.z);
|
|
30
|
+
}
|
|
31
|
+
float jgSurfNoise(vec3 x) {
|
|
32
|
+
vec3 i = floor(x);
|
|
33
|
+
vec3 f = fract(x);
|
|
34
|
+
vec3 u = f * f * (3.0 - 2.0 * f);
|
|
35
|
+
return mix(
|
|
36
|
+
mix(mix(jgSurfHash(i), jgSurfHash(i + vec3(1, 0, 0)), u.x), mix(jgSurfHash(i + vec3(0, 1, 0)), jgSurfHash(i + vec3(1, 1, 0)), u.x), u.y),
|
|
37
|
+
mix(mix(jgSurfHash(i + vec3(0, 0, 1)), jgSurfHash(i + vec3(1, 0, 1)), u.x), mix(jgSurfHash(i + vec3(0, 1, 1)), jgSurfHash(i + vec3(1, 1, 1)), u.x), u.y),
|
|
38
|
+
u.z);
|
|
39
|
+
}`;
|
|
40
|
+
// Three octaves of object-space value noise, each faded once a pixel spans its cell so distance never
|
|
41
|
+
// shimmers: broad mottling, brush-scale variation and fine speckle. Wear lifts raised spots paler and drier.
|
|
42
|
+
const COLOR_MAIN = /* glsl */ `
|
|
43
|
+
#include <color_fragment>
|
|
44
|
+
float jgFootprint = max(length(dFdx(vJgSurfPos)), length(dFdy(vJgSurfPos)));
|
|
45
|
+
float jgBroad = jgSurfNoise(vJgSurfPos * 1.6);
|
|
46
|
+
float jgMid = mix(0.5, jgSurfNoise(vJgSurfPos * 4.5 + 7.0), 1.0 - smoothstep(0.12, 0.4, jgFootprint * 4.5));
|
|
47
|
+
float jgFine = mix(0.5, jgSurfNoise(vJgSurfPos * 12.0 + 17.0), 1.0 - smoothstep(0.12, 0.4, jgFootprint * 12.0));
|
|
48
|
+
float jgWear = smoothstep(0.56, 0.8, jgBroad * 0.45 + jgMid * 0.4 + jgFine * 0.15) * jgSurfStrength;
|
|
49
|
+
float jgLuma = dot(diffuseColor.rgb, vec3(0.299, 0.587, 0.114));
|
|
50
|
+
diffuseColor.rgb *= 1.0 + ((jgBroad - 0.5) * 0.2 + (jgMid - 0.5) * 0.16 + (jgFine - 0.5) * 0.1) * jgSurfStrength;
|
|
51
|
+
diffuseColor.rgb = mix(diffuseColor.rgb, mix(vec3(jgLuma), diffuseColor.rgb, 0.7) * 1.18, jgWear * 0.5);`;
|
|
52
|
+
const ROUGHNESS_MAIN = /* glsl */ `
|
|
53
|
+
#include <roughnessmap_fragment>
|
|
54
|
+
roughnessFactor = clamp(roughnessFactor + ((jgMid - 0.5) * 0.24 + (jgFine - 0.5) * 0.12 + jgWear * 0.08) * jgSurfStrength, 0.04, 1.0);`;
|
|
55
|
+
// Mikkelsen's screen-space bump with unnormalized frame vectors, so the height is in metres and the
|
|
56
|
+
// slope reads the same at every distance; fades before the grain aliases.
|
|
57
|
+
const NORMAL_MAIN = /* glsl */ `
|
|
58
|
+
#include <normal_fragment_maps>
|
|
59
|
+
{
|
|
60
|
+
float jgGrainFade = 1.0 - smoothstep(0.12, 0.4, jgFootprint * 34.0);
|
|
61
|
+
float jgHeight = (jgMid * 0.012 + jgFine * 0.006 + jgSurfNoise(vJgSurfPos * 34.0 + 41.0) * 0.003 * jgGrainFade) * jgSurfStrength;
|
|
62
|
+
vec3 jgDpdx = dFdx(-vViewPosition);
|
|
63
|
+
vec3 jgDpdy = dFdy(-vViewPosition);
|
|
64
|
+
vec3 jgR1 = cross(jgDpdy, normal);
|
|
65
|
+
vec3 jgR2 = cross(normal, jgDpdx);
|
|
66
|
+
float jgDet = dot(jgDpdx, jgR1) * faceDirection;
|
|
67
|
+
vec3 jgGrad = sign(jgDet) * (dFdx(jgHeight) * jgR1 + dFdy(jgHeight) * jgR2);
|
|
68
|
+
vec3 jgBumped = abs(jgDet) * normal - jgGrad;
|
|
69
|
+
float jgLength2 = dot(jgBumped, jgBumped);
|
|
70
|
+
// A degenerate pixel frame (det 0, edge-on or helper lanes) would normalize zero into NaN, which bloom spreads across the frame.
|
|
71
|
+
if (jgLength2 > 1e-30 && jgLength2 < 1e30) normal = jgBumped * inversesqrt(jgLength2);
|
|
72
|
+
}`;
|
|
73
|
+
// Cheap cavity: down-facing and mottled-dark spots receive less sky, which grounds a form without an AO pass.
|
|
74
|
+
const OCCLUSION_MAIN = /* glsl */ `
|
|
75
|
+
#include <aomap_fragment>
|
|
76
|
+
{
|
|
77
|
+
float jgUp = inverseTransformDirection(normal, viewMatrix).y * 0.5 + 0.5;
|
|
78
|
+
float jgCavity = mix(1.0, mix(0.7, 1.0, jgUp) * (0.88 + 0.12 * jgBroad), jgSurfStrength);
|
|
79
|
+
reflectedLight.indirectDiffuse *= jgCavity;
|
|
80
|
+
reflectedLight.indirectSpecular *= jgCavity;
|
|
81
|
+
}`;
|
|
82
|
+
function isStandard(material) {
|
|
83
|
+
return material.isMeshStandardMaterial === true;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Whether the procedural surface pass suits this material: an opaque standard/physical surface without
|
|
87
|
+
* its own normal map (flat colour, vertex colour, or a palette-atlas texture like KayKit's).
|
|
88
|
+
* @internal
|
|
89
|
+
*/
|
|
90
|
+
export function modelSurfaceEligible(material) {
|
|
91
|
+
return isStandard(material) && material.normalMap === null && !material.transparent && material.alphaTest === 0;
|
|
92
|
+
}
|
|
93
|
+
/** The level a material compiles with right now: the global tier unless the material opted out. @internal */
|
|
94
|
+
export function materialSurfaceLevel(material) {
|
|
95
|
+
if (material.userData[OPT_OUT_KEY] === false || !modelSurfaceEligible(material))
|
|
96
|
+
return "off";
|
|
97
|
+
return activeLevel;
|
|
98
|
+
}
|
|
99
|
+
function surfaceOnBeforeCompile(shader) {
|
|
100
|
+
if (!compiled.has(this)) {
|
|
101
|
+
compiled.add(this);
|
|
102
|
+
const material = this;
|
|
103
|
+
material.addEventListener("dispose", function release() {
|
|
104
|
+
material.removeEventListener("dispose", release);
|
|
105
|
+
compiled.delete(material);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const level = materialSurfaceLevel(this);
|
|
109
|
+
if (level === "off")
|
|
110
|
+
return;
|
|
111
|
+
shader.uniforms.jgSurfStrength = strengthUniform;
|
|
112
|
+
shader.vertexShader = shader.vertexShader
|
|
113
|
+
.replace("#include <common>", `#include <common>${VERTEX_PARS}`)
|
|
114
|
+
.replace("#include <skinning_vertex>", VERTEX_MAIN);
|
|
115
|
+
let fragment = shader.fragmentShader
|
|
116
|
+
.replace("#include <common>", `#include <common>${FRAGMENT_PARS}`)
|
|
117
|
+
.replace("#include <color_fragment>", COLOR_MAIN)
|
|
118
|
+
.replace("#include <roughnessmap_fragment>", ROUGHNESS_MAIN)
|
|
119
|
+
.replace("#include <aomap_fragment>", OCCLUSION_MAIN);
|
|
120
|
+
if (level === "full")
|
|
121
|
+
fragment = fragment.replace("#include <normal_fragment_maps>", NORMAL_MAIN);
|
|
122
|
+
shader.fragmentShader = fragment;
|
|
123
|
+
}
|
|
124
|
+
function surfaceCacheKey() {
|
|
125
|
+
return `jg-surface:v1:${materialSurfaceLevel(this)}`;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Re-tune a glTF material's exported finish into the painted range: glossy 0.4–0.5 roughness reads as
|
|
129
|
+
* plastic, and metallic 1 / roughness 1 with no map is an exporter default that renders near-black.
|
|
130
|
+
* @internal
|
|
131
|
+
*/
|
|
132
|
+
export function normalizeModelMaterial(material) {
|
|
133
|
+
if (material.userData[ORIGINAL_KEY] !== undefined)
|
|
134
|
+
return;
|
|
135
|
+
const original = { roughness: material.roughness, metalness: material.metalness, envMapIntensity: material.envMapIntensity };
|
|
136
|
+
material.userData[ORIGINAL_KEY] = original;
|
|
137
|
+
if (material.roughnessMap === null)
|
|
138
|
+
material.roughness = 0.6 + 0.35 * THREE.MathUtils.clamp(material.roughness, 0, 1);
|
|
139
|
+
if (material.metalnessMap === null) {
|
|
140
|
+
material.metalness = original.metalness >= 0.99 && original.roughness >= 0.99 ? 0.1 : Math.min(material.metalness, 0.85);
|
|
141
|
+
}
|
|
142
|
+
material.envMapIntensity = Math.min(material.envMapIntensity, 0.8);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Give every eligible material in a freshly loaded model the engine's default painted finish: exported
|
|
146
|
+
* PBR params normalized and a shared, tier-aware procedural detail shader (object-space mottling, wear,
|
|
147
|
+
* roughness break-up, micro-normal and cavity). Idempotent; materials with their own normal maps, cutout
|
|
148
|
+
* or transparent materials and non-standard materials are left untouched.
|
|
149
|
+
* @internal
|
|
150
|
+
*/
|
|
151
|
+
export function applyModelSurface(root) {
|
|
152
|
+
if (!gameEnabled)
|
|
153
|
+
return;
|
|
154
|
+
const seen = new Set();
|
|
155
|
+
root.traverse((node) => {
|
|
156
|
+
const mesh = node;
|
|
157
|
+
if (!mesh.isMesh)
|
|
158
|
+
return;
|
|
159
|
+
for (const material of Array.isArray(mesh.material) ? mesh.material : [mesh.material]) {
|
|
160
|
+
if (seen.has(material) || !modelSurfaceEligible(material))
|
|
161
|
+
continue;
|
|
162
|
+
seen.add(material);
|
|
163
|
+
normalizeModelMaterial(material);
|
|
164
|
+
if (material.onBeforeCompile === surfaceOnBeforeCompile)
|
|
165
|
+
continue;
|
|
166
|
+
if (material.onBeforeCompile !== THREE.Material.prototype.onBeforeCompile)
|
|
167
|
+
continue;
|
|
168
|
+
material.onBeforeCompile = surfaceOnBeforeCompile;
|
|
169
|
+
material.customProgramCacheKey = surfaceCacheKey;
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Opt one cloned material out of the default surface: restore its exported params and compile it
|
|
175
|
+
* without the detail shader. Used for a model whose `surfaceDetail` is `false`.
|
|
176
|
+
* @internal
|
|
177
|
+
*/
|
|
178
|
+
export function optOutModelSurface(material) {
|
|
179
|
+
const original = material.userData[ORIGINAL_KEY];
|
|
180
|
+
material.userData[OPT_OUT_KEY] = false;
|
|
181
|
+
if (original !== undefined && isStandard(material)) {
|
|
182
|
+
material.roughness = original.roughness;
|
|
183
|
+
material.metalness = original.metalness;
|
|
184
|
+
material.envMapIntensity = original.envMapIntensity;
|
|
185
|
+
}
|
|
186
|
+
material.needsUpdate = true;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Select the surface-detail level every patched model material compiles with, and whether models loaded
|
|
190
|
+
* from now on get the default finish at all (a game's `surfaceDetail: false`). Changing the level
|
|
191
|
+
* recompiles only materials already drawn; programs are shared per level, so the count stays bounded.
|
|
192
|
+
* @internal
|
|
193
|
+
*/
|
|
194
|
+
export function configureModelSurface(enabled, level) {
|
|
195
|
+
gameEnabled = enabled;
|
|
196
|
+
const next = enabled ? level : "off";
|
|
197
|
+
if (next === activeLevel)
|
|
198
|
+
return;
|
|
199
|
+
activeLevel = next;
|
|
200
|
+
for (const material of compiled)
|
|
201
|
+
material.needsUpdate = true;
|
|
202
|
+
}
|
|
203
|
+
/** Current model surface-detail level. @internal */
|
|
204
|
+
export function modelSurfaceLevel() {
|
|
205
|
+
return activeLevel;
|
|
206
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Group, type Object3D } from "three";
|
|
2
2
|
import type { ModelConfig } from "@jgengine/core/game/playableGame";
|
|
3
3
|
/** Model loading, placement, shadows and animation for a custom renderer. */
|
|
4
|
-
export type ModelInstanceConfig = Pick<ModelConfig, "url" | "scale" | "targetHeight" | "y" | "anchor" | "dims" | "shadows" | "animation" | "hiddenNodes">;
|
|
4
|
+
export type ModelInstanceConfig = Pick<ModelConfig, "url" | "scale" | "targetHeight" | "y" | "anchor" | "dims" | "shadows" | "animation" | "hiddenNodes" | "surfaceDetail">;
|
|
5
5
|
/** Caller-owned styling runs on the isolated clone, never the loader cache. */
|
|
6
6
|
export interface ModelInstanceOptions {
|
|
7
7
|
instanceId?: string;
|
|
@@ -23,7 +23,7 @@ export function useModelInstance(model, options = {}) {
|
|
|
23
23
|
const { configure, instanceId } = options;
|
|
24
24
|
const hiddenNodesKey = modelHiddenNodesKey(hiddenNodes);
|
|
25
25
|
const { content, scene, bindBounds } = useMemo(() => {
|
|
26
|
-
const content = cloneModelScene(gltf.scene, { shadows: model.shadows });
|
|
26
|
+
const content = cloneModelScene(gltf.scene, { shadows: model.shadows, surfaceDetail: model.surfaceDetail });
|
|
27
27
|
try {
|
|
28
28
|
configure?.(content);
|
|
29
29
|
for (const diagnostic of applyModelNodeVisibility(content, hiddenNodes))
|
|
@@ -35,7 +35,7 @@ export function useModelInstance(model, options = {}) {
|
|
|
35
35
|
disposeModelScene(content);
|
|
36
36
|
throw error;
|
|
37
37
|
}
|
|
38
|
-
}, [gltf.scene, model.shadows, configure, hiddenNodesKey]);
|
|
38
|
+
}, [gltf.scene, model.shadows, model.surfaceDetail, configure, hiddenNodesKey]);
|
|
39
39
|
const placement = useMemo(() => modelPlacementTransform(scene, model, bindBounds), [scene, bindBounds, model.scale, model.targetHeight, model.y, model.anchor, model.dims]);
|
|
40
40
|
useEffect(() => () => disposeModelScene(content), [content]);
|
|
41
41
|
useModelAnimation(content, gltf.animations, model.animation, instanceId);
|
|
@@ -2,6 +2,7 @@ import * as THREE from "three";
|
|
|
2
2
|
import { parseModelHiddenNodes } from "@jgengine/core/editor/modelNodeVisibility";
|
|
3
3
|
import { applyMaterialOverride } from "../materialOverride.js";
|
|
4
4
|
import { cloneModelScene, disposeModelScene, ownModelMaterial } from "../render/modelRender.js";
|
|
5
|
+
import { applyModelSurface } from "../render/modelSurface.js";
|
|
5
6
|
/** The scale + position offset a `ModelConfig` applies before any per-instance transform — same anchor/targetHeight rules as the single-instance entity renderer. */
|
|
6
7
|
function scatterModelBaseTransform(scene, model) {
|
|
7
8
|
let scale = model.scale ?? 1;
|
|
@@ -40,7 +41,8 @@ export function buildScatterModelSources(gltfScene, model) {
|
|
|
40
41
|
throw new Error("Instanced model material assignments require the individual model renderer; use EntityModel for authored slots");
|
|
41
42
|
if (model.material?.maps !== undefined)
|
|
42
43
|
throw new Error("Instanced model override maps require an owned texture loader; use EntityModel for authored maps");
|
|
43
|
-
|
|
44
|
+
applyModelSurface(gltfScene);
|
|
45
|
+
const root = cloneModelScene(gltfScene, { shadows: model.shadows, surfaceDetail: model.surfaceDetail });
|
|
44
46
|
if (model.material !== undefined)
|
|
45
47
|
applyMaterialOverride(root, model.material, { clone: false, ownMaterial: material => ownModelMaterial(root, material) });
|
|
46
48
|
root.updateMatrixWorld(true);
|
|
@@ -16,13 +16,19 @@ export interface AuthoredPathsProps {
|
|
|
16
16
|
field: TerrainField;
|
|
17
17
|
/** Only render these path kinds; default renders every non-scatter path. */
|
|
18
18
|
kinds?: readonly string[];
|
|
19
|
+
/**
|
|
20
|
+
* Shade paths with the terrain's procedural surfaces (dirt, gravel, paving), wear and soft edges.
|
|
21
|
+
* Default true; false keeps flat-colour ribbons. One path opts out with `meta.surfaceDetail: false`.
|
|
22
|
+
*/
|
|
23
|
+
surfaceDetail?: boolean;
|
|
19
24
|
}
|
|
20
25
|
/**
|
|
21
26
|
* Renders a document's non-scatter paths (roads, routes, corridors) as ground-draped ribbons — the
|
|
22
27
|
* editor authors the polyline, the engine drapes it over the live terrain at runtime. Width comes
|
|
23
|
-
* from `path.width`, color from `path.meta.color`/`path.color
|
|
28
|
+
* from `path.width`, color from `path.meta.color`/`path.color`, and the procedural surface from
|
|
29
|
+
* `path.meta.surface` or the kind (see `resolvePathSurface`). A game never hand-rolls path meshes.
|
|
24
30
|
*/
|
|
25
|
-
export declare function AuthoredPaths({ document, field, kinds }: AuthoredPathsProps): import("react").JSX.Element;
|
|
31
|
+
export declare function AuthoredPaths({ document, field, kinds, surfaceDetail }: AuthoredPathsProps): import("react").JSX.Element;
|
|
26
32
|
/** Props for {@link AuthoredObjects}: document, ground field, and optional lift / onExisting. */
|
|
27
33
|
export interface AuthoredObjectsProps extends ResolveAuthoredObjectsOptions {
|
|
28
34
|
document: EditorDocument;
|
|
@@ -60,6 +66,8 @@ export interface AuthoredSceneProps {
|
|
|
60
66
|
field: TerrainField;
|
|
61
67
|
/** Restrict rendered path kinds; default renders every non-scatter path. */
|
|
62
68
|
pathKinds?: readonly string[];
|
|
69
|
+
/** Procedural surface, wear and soft edges on paths (default true); see {@link AuthoredPathsProps.surfaceDetail}. */
|
|
70
|
+
pathSurfaceDetail?: boolean;
|
|
63
71
|
/**
|
|
64
72
|
* Scatter palette item id → model asset id or `ModelConfig`; a mapped item GPU-instances the real
|
|
65
73
|
* catalog GLB instead of the stylized proxy. Keyed by scatter `item` (not entity name — pass a
|
|
@@ -98,4 +106,4 @@ export interface AuthoredSceneProps {
|
|
|
98
106
|
* place markers with your own entity spawns. Pass `scatterModels`+`assets` to resolve palette items to
|
|
99
107
|
* real catalog GLBs; unmapped items keep the stylized proxy.
|
|
100
108
|
*/
|
|
101
|
-
export declare function AuthoredScene({ document, diagnostics, field, pathKinds, scatterModels, flockModels, assets, live, placeObjects, groundColorAt, }: AuthoredSceneProps): import("react").JSX.Element;
|
|
109
|
+
export declare function AuthoredScene({ document, diagnostics, field, pathKinds, pathSurfaceDetail, scatterModels, flockModels, assets, live, placeObjects, groundColorAt, }: AuthoredSceneProps): import("react").JSX.Element;
|
|
@@ -10,6 +10,8 @@ import { buildRoadRibbon, GROUND_DECAL_LAYERS, roundPathCorners } from "@jgengin
|
|
|
10
10
|
import { isScatterPath, resolveScatter } from "@jgengine/core/world/scatterRegion";
|
|
11
11
|
import { getAssetGenerator } from "@jgengine/core/scene/assetGenerator";
|
|
12
12
|
import { useGameContext } from "@jgengine/react/provider";
|
|
13
|
+
import { resolvePathSurface, RIBBON_FRAME_ATTRIBUTE, ribbonEdgeMargin, ribbonSurfaceFrame, useSurfaceRibbonMaterial, } from "../terrain/surfaceRibbonMaterial.js";
|
|
14
|
+
import { useTerrainQuality } from "../terrain/terrainQuality.js";
|
|
13
15
|
import { createModelMapResolver } from "../render/resolveModel.js";
|
|
14
16
|
import { InstancedScatter } from "../scatter/InstancedScatter.js";
|
|
15
17
|
import { GeneratedAssetInstance } from "./GeneratedAssetRenderer.js";
|
|
@@ -93,26 +95,31 @@ function AuthoredStudios({ document, context }) {
|
|
|
93
95
|
}
|
|
94
96
|
const DEFAULT_PATH_COLOR = "#7a6444";
|
|
95
97
|
/** One editor path drawn as a ground-draped ribbon — subdivided and sampled against the live field. */
|
|
96
|
-
function DrapedPath({ points, width, color, field, }) {
|
|
98
|
+
function DrapedPath({ points, width, color, field, surface, }) {
|
|
99
|
+
const quality = useTerrainQuality();
|
|
100
|
+
const margin = surface === null ? 0 : ribbonEdgeMargin(surface);
|
|
97
101
|
const geometry = useMemo(() => {
|
|
98
102
|
// Fillet sharp turns first so bends read as smooth arcs, not overlapping rectangles.
|
|
99
103
|
const rounded = roundPathCorners(points, Math.max(1.5, width * 0.9), 6);
|
|
100
|
-
|
|
104
|
+
// A soft edge needs the ribbon wider than authored so the fade straddles the authored edge.
|
|
105
|
+
const ribbon = buildRoadRibbon(rounded, width + margin, (x, z) => field.sampleHeight(x, z), {
|
|
101
106
|
// Generic authored paths (dirt roads, routes, corridors) drape on the shared road decal layer.
|
|
102
107
|
elevation: GROUND_DECAL_LAYERS.road,
|
|
103
108
|
maxSegmentLength: 1,
|
|
104
109
|
});
|
|
105
110
|
const geo = new THREE.BufferGeometry();
|
|
106
111
|
geo.setAttribute("position", new THREE.BufferAttribute(ribbon.positions, 3));
|
|
112
|
+
geo.setAttribute(RIBBON_FRAME_ATTRIBUTE, new THREE.BufferAttribute(ribbonSurfaceFrame([ribbon.positions]), 3));
|
|
107
113
|
geo.setIndex(new THREE.BufferAttribute(ribbon.indices, 1));
|
|
108
114
|
geo.computeVertexNormals();
|
|
109
115
|
return geo;
|
|
110
116
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
111
|
-
}, [points, width, field]);
|
|
117
|
+
}, [points, width, margin, field]);
|
|
112
118
|
useEffect(() => () => geometry.dispose(), [geometry]);
|
|
119
|
+
const material = useSurfaceRibbonMaterial(surface === null ? null : { ...surface, color, halfWidth: width / 2, quality });
|
|
113
120
|
if (points.length < 2)
|
|
114
121
|
return null;
|
|
115
|
-
return (_jsx("mesh", { geometry: geometry, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: color, roughness: 1, metalness: 0, side: THREE.DoubleSide }) }));
|
|
122
|
+
return (_jsx("mesh", { geometry: geometry, receiveShadow: true, ...(material === null ? {} : { material }), children: material === null ? _jsx("meshStandardMaterial", { color: color, roughness: 1, metalness: 0, side: THREE.DoubleSide }) : null }));
|
|
116
123
|
}
|
|
117
124
|
function pathColor(meta, color) {
|
|
118
125
|
if (typeof meta?.["color"] === "string")
|
|
@@ -122,11 +129,12 @@ function pathColor(meta, color) {
|
|
|
122
129
|
/**
|
|
123
130
|
* Renders a document's non-scatter paths (roads, routes, corridors) as ground-draped ribbons — the
|
|
124
131
|
* editor authors the polyline, the engine drapes it over the live terrain at runtime. Width comes
|
|
125
|
-
* from `path.width`, color from `path.meta.color`/`path.color
|
|
132
|
+
* from `path.width`, color from `path.meta.color`/`path.color`, and the procedural surface from
|
|
133
|
+
* `path.meta.surface` or the kind (see `resolvePathSurface`). A game never hand-rolls path meshes.
|
|
126
134
|
*/
|
|
127
|
-
export function AuthoredPaths({ document, field, kinds }) {
|
|
135
|
+
export function AuthoredPaths({ document, field, kinds, surfaceDetail = true }) {
|
|
128
136
|
const paths = document.paths.filter((path) => !isScatterPath(path) && getSceneKindRenderer(path.kind) === undefined && (kinds === undefined || kinds.includes(path.kind)));
|
|
129
|
-
return (_jsx(_Fragment, { children: paths.map((path) => (_jsx(DrapedPath, { points: path.points.map((point) => [point.x, point.z]), width: path.width ?? 4, color: pathColor(path.meta, path.color), field: field }, path.id))) }));
|
|
137
|
+
return (_jsx(_Fragment, { children: paths.map((path) => (_jsx(DrapedPath, { points: path.points.map((point) => [point.x, point.z]), width: path.width ?? 4, color: pathColor(path.meta, path.color), field: field, surface: surfaceDetail ? resolvePathSurface(path.kind, path.width ?? 4, path.meta) : null }, path.id))) }));
|
|
130
138
|
}
|
|
131
139
|
/**
|
|
132
140
|
* Places every editor marker with a catalog id into `ctx.scene.object`, grounded on `field`.
|
|
@@ -186,7 +194,7 @@ export function AuthoredSolids({ document, field }) {
|
|
|
186
194
|
* place markers with your own entity spawns. Pass `scatterModels`+`assets` to resolve palette items to
|
|
187
195
|
* real catalog GLBs; unmapped items keep the stylized proxy.
|
|
188
196
|
*/
|
|
189
|
-
export function AuthoredScene({ document, diagnostics = false, field, pathKinds, scatterModels, flockModels, assets, live = true, placeObjects, groundColorAt, }) {
|
|
197
|
+
export function AuthoredScene({ document, diagnostics = false, field, pathKinds, pathSurfaceDetail = true, scatterModels, flockModels, assets, live = true, placeObjects, groundColorAt, }) {
|
|
190
198
|
const ctx = useGameContext();
|
|
191
199
|
const liveDocument = useLiveEditorDocument(document, live);
|
|
192
200
|
// Authoring updates configuration only. Physics remains owned by the fixed game-time stages.
|
|
@@ -196,7 +204,7 @@ export function AuthoredScene({ document, diagnostics = false, field, pathKinds,
|
|
|
196
204
|
const shouldPlaceObjects = placeObjects === true || (typeof placeObjects === "object" && placeObjects !== null);
|
|
197
205
|
const objectVerticalOffset = typeof placeObjects === "object" && placeObjects !== null ? (placeObjects.verticalOffset ?? 0) : 0;
|
|
198
206
|
const objectExcludeKinds = typeof placeObjects === "object" && placeObjects !== null ? placeObjects.excludeKinds : undefined;
|
|
199
|
-
return (_jsxs(_Fragment, { children: [_jsx(AuthoredWeatherLayer, { document: liveDocument, heightAt: field.sampleHeight, diagnostics: diagnostics }), flockModels === undefined ? null : _jsx(AuthoredFlocks, { models: flockModels, assets: assets }), _jsx(AuthoredPaths, { document: liveDocument, field: field, ...(pathKinds === undefined ? {} : { kinds: pathKinds }) }), _jsx(InstancedScatter, { instances: instances, ...(resolveItem === undefined ? {} : { resolveItem }) }), _jsx(AuthoredStudios, { document: liveDocument, context: {
|
|
207
|
+
return (_jsxs(_Fragment, { children: [_jsx(AuthoredWeatherLayer, { document: liveDocument, heightAt: field.sampleHeight, diagnostics: diagnostics }), flockModels === undefined ? null : _jsx(AuthoredFlocks, { models: flockModels, assets: assets }), _jsx(AuthoredPaths, { document: liveDocument, field: field, surfaceDetail: pathSurfaceDetail, ...(pathKinds === undefined ? {} : { kinds: pathKinds }) }), _jsx(InstancedScatter, { instances: instances, ...(resolveItem === undefined ? {} : { resolveItem }) }), _jsx(AuthoredStudios, { document: liveDocument, context: {
|
|
200
208
|
document: liveDocument,
|
|
201
209
|
field,
|
|
202
210
|
...(assets === undefined ? {} : { assets }),
|
|
@@ -24,11 +24,15 @@ import { resolveCityObject, CITY_KIND, CITY_ZONE_KIND, } from "@jgengine/core/wo
|
|
|
24
24
|
import { zoneBand, zoneMetric } from "@jgengine/core/world/cityContent";
|
|
25
25
|
import { generateBuilding, buildingSurfaceColor, resolveBuildingPalette, resolveBuildingWallTones, } from "@jgengine/core/world/buildings";
|
|
26
26
|
import { hashString } from "@jgengine/core/random/rng";
|
|
27
|
+
import { valueNoise } from "@jgengine/core/world/terrain";
|
|
27
28
|
import { buildScatterProxy } from "../scatter/scatterProxies.js";
|
|
28
29
|
import { InstancedBuildings } from "../structures/GeneratedBuilding.js";
|
|
29
30
|
import { getBuildingKit } from "../structures/buildingKitRegistry.js";
|
|
30
31
|
import { createCityWindowMaterial } from "./cityWindowMaterial.js";
|
|
32
|
+
import { createSurfaceRibbonMaterial } from "../terrain/surfaceRibbonMaterial.js";
|
|
33
|
+
import { useTerrainQuality } from "../terrain/terrainQuality.js";
|
|
31
34
|
import { registerSceneKindRenderer } from "./sceneKindRenderers.js";
|
|
35
|
+
import { junctionSurface } from "./cityJunctionSurface.js";
|
|
32
36
|
const ASPHALT_COLOR = "#3a3b3d";
|
|
33
37
|
const GRAVEL_COLOR = "#93815f";
|
|
34
38
|
const SIDEWALK_COLOR = "#867f74";
|
|
@@ -58,8 +62,8 @@ const CROP_COLORS = ["#5d7a35", "#7a6b41", "#6f7f3a"];
|
|
|
58
62
|
* only a low-density (large-block) district lets the edge band go back to lawn. */
|
|
59
63
|
const BLOCK_SURFACE_COLORS = { core: "#5b5850", mid: "#6c6456", edge: "#78765c" };
|
|
60
64
|
const BLOCK_EDGE_GREEN = "#6a7f4a";
|
|
61
|
-
/**
|
|
62
|
-
const
|
|
65
|
+
/** Width (m) over which the developed ground fades out at its outer reach, so it has no stamped rim. */
|
|
66
|
+
const FABRIC_FADE = 9;
|
|
63
67
|
/** Non-buildable block land takes its open-space colour so a park block never shows raw terrain. */
|
|
64
68
|
const BLOCK_OPEN_COLORS = { park: PARK_COLORS.green, plaza: PARK_COLORS.plaza, field: PARK_COLORS.field, buffer: PARK_COLORS.buffer };
|
|
65
69
|
/**
|
|
@@ -573,7 +577,7 @@ function OneCity({ object, context }) {
|
|
|
573
577
|
const asphalt = new MeshAccumulator(true);
|
|
574
578
|
const gravel = new MeshAccumulator();
|
|
575
579
|
const sidewalks = new MeshAccumulator();
|
|
576
|
-
|
|
580
|
+
let urban = null;
|
|
577
581
|
const land = new MeshAccumulator(true);
|
|
578
582
|
const patches = new MeshAccumulator(true);
|
|
579
583
|
const markings = new MeshAccumulator(true);
|
|
@@ -706,7 +710,11 @@ function OneCity({ object, context }) {
|
|
|
706
710
|
const crossIndex = trimmed.junctionIndices[j];
|
|
707
711
|
const scuff = (hashString(`cross:${crossIndex}`) % 512) / 512;
|
|
708
712
|
wearColor.setRGB(0.76 + scuff * 0.18, 0.77 + scuff * 0.17, 0.8 + scuff * 0.16);
|
|
709
|
-
|
|
713
|
+
const cross = resolved.intersections[crossIndex];
|
|
714
|
+
if (cross !== undefined && junctionSurface(cross, resolved.streets) === "gravel")
|
|
715
|
+
gravel.addMesh(trimmed.junctions[j]);
|
|
716
|
+
else
|
|
717
|
+
patches.addMesh(trimmed.junctions[j], wearColor);
|
|
710
718
|
}
|
|
711
719
|
for (const paint of buildIntersectionMarkings(trimmed, deckSampler, {
|
|
712
720
|
mouthClearance: 1.25,
|
|
@@ -846,32 +854,59 @@ function OneCity({ object, context }) {
|
|
|
846
854
|
}
|
|
847
855
|
}
|
|
848
856
|
}
|
|
849
|
-
|
|
857
|
+
// One shared-vertex sheet coloured per vertex from smooth noise: a per-cell roll flipped
|
|
858
|
+
// neighbouring cells between zone bands and drew a hard checkerboard over the district.
|
|
850
859
|
const fabricColor = new THREE.Color();
|
|
860
|
+
const seed = hashString(`fabric:${resolved.center[0]}:${resolved.center[2]}`);
|
|
861
|
+
const vertexIndex = new Int32Array(nx * nz).fill(-1);
|
|
862
|
+
const fabric = { positions: [], indices: [], colors: [] };
|
|
863
|
+
const vertexAt = (i, j) => {
|
|
864
|
+
const idx = j * nx + i;
|
|
865
|
+
const existing = vertexIndex[idx];
|
|
866
|
+
if (existing >= 0)
|
|
867
|
+
return existing;
|
|
868
|
+
const x = originX + i * cell;
|
|
869
|
+
const z = originZ + j * cell;
|
|
870
|
+
const d = field[idx];
|
|
871
|
+
const tone = valueNoise(x / 9, z / 9, seed) * 0.5 + 0.5;
|
|
872
|
+
if (d < pavedTo) {
|
|
873
|
+
fabricColor.set(SIDEWALK_COLOR);
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
const band = bandAt(x, z, tone);
|
|
877
|
+
fabricColor.set(band === "edge" && lawnEdge ? BLOCK_EDGE_GREEN : BLOCK_SURFACE_COLORS[band]);
|
|
878
|
+
// A block-sized tone over the fine grain: yards, courts and lots, not one poured apron.
|
|
879
|
+
fabricColor.multiplyScalar(0.8 + (valueNoise(x / 22, z / 22, seed + 1) * 0.5 + 0.5) * 0.42);
|
|
880
|
+
}
|
|
881
|
+
fabricColor.multiplyScalar(0.95 + tone * 0.1);
|
|
882
|
+
// Fade to transparent over the outer reach so the sheet melts into whatever terrain lies
|
|
883
|
+
// beyond it, instead of a hard stair-stepped rim in a fixed fringe colour.
|
|
884
|
+
const t = Math.max(0, Math.min(1, (reach - d) / FABRIC_FADE));
|
|
885
|
+
const next = fabric.positions.length / 3;
|
|
886
|
+
fabric.positions.push(x, sample(x, z) + URBAN_LAYER, z);
|
|
887
|
+
fabric.colors.push(fabricColor.r, fabricColor.g, fabricColor.b, t * t * (3 - 2 * t));
|
|
888
|
+
vertexIndex[idx] = next;
|
|
889
|
+
return next;
|
|
890
|
+
};
|
|
851
891
|
for (let j = 0; j + 1 < nz; j += 1) {
|
|
852
892
|
for (let i = 0; i + 1 < nx; i += 1) {
|
|
853
893
|
const d = (field[j * nx + i] + field[j * nx + i + 1] + field[(j + 1) * nx + i] + field[(j + 1) * nx + i + 1]) / 4;
|
|
854
894
|
if (d > reach)
|
|
855
895
|
continue;
|
|
856
|
-
const
|
|
857
|
-
const
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
}
|
|
862
|
-
else {
|
|
863
|
-
const band = bandAt(x, z, tone);
|
|
864
|
-
fabricColor.set(band === "edge" && lawnEdge ? BLOCK_EDGE_GREEN : BLOCK_SURFACE_COLORS[band]);
|
|
865
|
-
// A block-sized patch tone over the fine per-cell grain: yards, courts and lots,
|
|
866
|
-
// not one poured apron.
|
|
867
|
-
fabricColor.multiplyScalar(0.8 + ((hashString(`yard:${Math.floor(x / 22)}:${Math.floor(z / 22)}`) % 256) / 256) * 0.42);
|
|
868
|
-
}
|
|
869
|
-
fabricColor.multiplyScalar(0.95 + tone * 0.1);
|
|
870
|
-
if (d > reach - 7)
|
|
871
|
-
fabricColor.lerp(fringe, Math.min(1, (d - (reach - 7)) / 7));
|
|
872
|
-
urban.addPatch([x, z], [cell, cell], 0, sample, URBAN_LAYER, fabricColor);
|
|
896
|
+
const a = vertexAt(i, j);
|
|
897
|
+
const b = vertexAt(i + 1, j);
|
|
898
|
+
const c = vertexAt(i, j + 1);
|
|
899
|
+
const e = vertexAt(i + 1, j + 1);
|
|
900
|
+
fabric.indices.push(a, c, b, b, c, e);
|
|
873
901
|
}
|
|
874
902
|
}
|
|
903
|
+
if (fabric.indices.length > 0) {
|
|
904
|
+
urban = new THREE.BufferGeometry();
|
|
905
|
+
urban.setAttribute("position", new THREE.BufferAttribute(new Float32Array(fabric.positions), 3));
|
|
906
|
+
urban.setAttribute("color", new THREE.BufferAttribute(new Float32Array(fabric.colors), 4));
|
|
907
|
+
urban.setIndex(fabric.indices);
|
|
908
|
+
urban.computeVertexNormals();
|
|
909
|
+
}
|
|
875
910
|
}
|
|
876
911
|
}
|
|
877
912
|
// The land ring behind the sidewalk is the block INTERIOR — fill it with a surface read from
|
|
@@ -941,7 +976,7 @@ function OneCity({ object, context }) {
|
|
|
941
976
|
asphalt: asphalt.build(),
|
|
942
977
|
gravel: gravel.build(),
|
|
943
978
|
sidewalks: sidewalks.build(),
|
|
944
|
-
urban
|
|
979
|
+
urban,
|
|
945
980
|
land: land.build(),
|
|
946
981
|
patches: patches.build(),
|
|
947
982
|
markings: markings.build(),
|
|
@@ -1469,9 +1504,36 @@ function OneCity({ object, context }) {
|
|
|
1469
1504
|
for (const material of race.materials)
|
|
1470
1505
|
material.dispose();
|
|
1471
1506
|
}, [race]);
|
|
1507
|
+
const quality = useTerrainQuality();
|
|
1508
|
+
const surfaceDetail = object.meta?.["surfaceDetail"] !== false;
|
|
1509
|
+
const groundMaterials = useMemo(() => {
|
|
1510
|
+
if (!surfaceDetail)
|
|
1511
|
+
return null;
|
|
1512
|
+
const make = (surface, color, vertexColors, roughness, scale = 1) => createSurfaceRibbonMaterial({ surface, color, vertexColors, roughness, scale, halfWidth: 1, quality });
|
|
1513
|
+
const urban = make("dirt", "#ffffff", true, 1);
|
|
1514
|
+
urban.transparent = true;
|
|
1515
|
+
urban.depthWrite = false;
|
|
1516
|
+
return {
|
|
1517
|
+
urban,
|
|
1518
|
+
land: make("dirt", "#ffffff", true, 1),
|
|
1519
|
+
asphalt: make("road", ASPHALT_COLOR, true, 0.96),
|
|
1520
|
+
gravel: make("gravel", GRAVEL_COLOR, false, 1),
|
|
1521
|
+
sidewalks: make("pavement", SIDEWALK_COLOR, false, 1, 1.6),
|
|
1522
|
+
patches: make("road", ASPHALT_COLOR, true, 0.96),
|
|
1523
|
+
medians: make("grass", MEDIAN_COLOR, false, 1),
|
|
1524
|
+
pavementDrives: make("concrete", PAVEMENT_COLOR, false, 1, 1.6),
|
|
1525
|
+
gravelDrives: make("gravel", GRAVEL_COLOR, false, 1),
|
|
1526
|
+
parks: make("ground", "#ffffff", true, 1),
|
|
1527
|
+
crops: make("dirt", "#ffffff", true, 1),
|
|
1528
|
+
};
|
|
1529
|
+
}, [surfaceDetail, quality]);
|
|
1530
|
+
useEffect(() => () => {
|
|
1531
|
+
for (const material of Object.values(groundMaterials ?? {}))
|
|
1532
|
+
material.dispose();
|
|
1533
|
+
}, [groundMaterials]);
|
|
1472
1534
|
if (resolved === null)
|
|
1473
1535
|
return null;
|
|
1474
|
-
return (_jsxs("group", { children: [ground?.urban != null ? (_jsx("mesh", { geometry: ground.urban, receiveShadow: true, children: _jsx("meshStandardMaterial", { vertexColors: true, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.land != null ? (_jsx("mesh", { geometry: ground.land, receiveShadow: true, children: _jsx("meshStandardMaterial", { vertexColors: true, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.asphalt != null ? (_jsx("mesh", { geometry: ground.asphalt, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: ASPHALT_COLOR, vertexColors: true, roughness: 0.96, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.gravel != null ? (_jsx("mesh", { geometry: ground.gravel, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: GRAVEL_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.sidewalks != null ? (_jsx("mesh", { geometry: ground.sidewalks, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: SIDEWALK_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.patches != null ? (_jsx("mesh", { geometry: ground.patches, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: ASPHALT_COLOR, vertexColors: true, roughness: 0.96, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.curbs != null ? (_jsx("mesh", { geometry: ground.curbs, receiveShadow: true, castShadow: true, children: _jsx("meshStandardMaterial", { color: CURB_COLOR, roughness: 0.9, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.markings != null ? (_jsx("mesh", { geometry: ground.markings, renderOrder: 1, children: _jsx("meshStandardMaterial", { color: MARKING_COLOR, vertexColors: true, roughness: 0.8, metalness: 0, side: THREE.DoubleSide, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 }) })) : null, ground?.medians != null ? (_jsx("mesh", { geometry: ground.medians, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: MEDIAN_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.pavementDrives != null ? (_jsx("mesh", { geometry: ground.pavementDrives, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: PAVEMENT_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.gravelDrives != null ? (_jsx("mesh", { geometry: ground.gravelDrives, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: GRAVEL_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.parks != null ? (_jsx("mesh", { geometry: ground.parks, receiveShadow: true, children: _jsx("meshStandardMaterial", { vertexColors: true, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.crops != null ? (_jsx("mesh", { geometry: ground.crops, receiveShadow: true, children: _jsx("meshStandardMaterial", { vertexColors: true, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, 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 ?? "" })] }));
|
|
1536
|
+
return (_jsxs("group", { children: [ground?.urban != null ? (_jsx("mesh", { geometry: ground.urban, receiveShadow: true, ...(groundMaterials === null ? {} : { material: groundMaterials.urban }), children: groundMaterials === null ? (_jsx("meshStandardMaterial", { vertexColors: true, transparent: true, depthWrite: false, roughness: 1, metalness: 0, side: THREE.DoubleSide })) : null })) : null, ground?.land != null ? (_jsx("mesh", { geometry: ground.land, receiveShadow: true, ...(groundMaterials === null ? {} : { material: groundMaterials.land }), children: groundMaterials === null ? _jsx("meshStandardMaterial", { vertexColors: true, roughness: 1, metalness: 0, side: THREE.DoubleSide }) : null })) : null, ground?.asphalt != null ? (_jsx("mesh", { geometry: ground.asphalt, receiveShadow: true, ...(groundMaterials === null ? {} : { material: groundMaterials.asphalt }), children: groundMaterials === null ? _jsx("meshStandardMaterial", { color: ASPHALT_COLOR, vertexColors: true, roughness: 0.96, metalness: 0, side: THREE.DoubleSide }) : null })) : null, ground?.gravel != null ? (_jsx("mesh", { geometry: ground.gravel, receiveShadow: true, ...(groundMaterials === null ? {} : { material: groundMaterials.gravel }), children: groundMaterials === null ? _jsx("meshStandardMaterial", { color: GRAVEL_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) : null })) : null, ground?.sidewalks != null ? (_jsx("mesh", { geometry: ground.sidewalks, receiveShadow: true, ...(groundMaterials === null ? {} : { material: groundMaterials.sidewalks }), children: groundMaterials === null ? _jsx("meshStandardMaterial", { color: SIDEWALK_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) : null })) : null, ground?.patches != null ? (_jsx("mesh", { geometry: ground.patches, receiveShadow: true, ...(groundMaterials === null ? {} : { material: groundMaterials.patches }), children: groundMaterials === null ? _jsx("meshStandardMaterial", { color: ASPHALT_COLOR, vertexColors: true, roughness: 0.96, metalness: 0, side: THREE.DoubleSide }) : null })) : null, ground?.curbs != null ? (_jsx("mesh", { geometry: ground.curbs, receiveShadow: true, castShadow: true, children: _jsx("meshStandardMaterial", { color: CURB_COLOR, roughness: 0.9, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.markings != null ? (_jsx("mesh", { geometry: ground.markings, renderOrder: 1, children: _jsx("meshStandardMaterial", { color: MARKING_COLOR, vertexColors: true, roughness: 0.8, metalness: 0, side: THREE.DoubleSide, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 }) })) : null, ground?.medians != null ? (_jsx("mesh", { geometry: ground.medians, receiveShadow: true, ...(groundMaterials === null ? {} : { material: groundMaterials.medians }), children: groundMaterials === null ? _jsx("meshStandardMaterial", { color: MEDIAN_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) : null })) : null, ground?.pavementDrives != null ? (_jsx("mesh", { geometry: ground.pavementDrives, receiveShadow: true, ...(groundMaterials === null ? {} : { material: groundMaterials.pavementDrives }), children: groundMaterials === null ? _jsx("meshStandardMaterial", { color: PAVEMENT_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) : null })) : null, ground?.gravelDrives != null ? (_jsx("mesh", { geometry: ground.gravelDrives, receiveShadow: true, ...(groundMaterials === null ? {} : { material: groundMaterials.gravelDrives }), children: groundMaterials === null ? _jsx("meshStandardMaterial", { color: GRAVEL_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) : null })) : null, ground?.parks != null ? (_jsx("mesh", { geometry: ground.parks, receiveShadow: true, ...(groundMaterials === null ? {} : { material: groundMaterials.parks }), children: groundMaterials === null ? _jsx("meshStandardMaterial", { vertexColors: true, roughness: 1, metalness: 0, side: THREE.DoubleSide }) : null })) : null, ground?.crops != null ? (_jsx("mesh", { geometry: ground.crops, receiveShadow: true, ...(groundMaterials === null ? {} : { material: groundMaterials.crops }), children: groundMaterials === null ? _jsx("meshStandardMaterial", { vertexColors: true, roughness: 1, metalness: 0, side: THREE.DoubleSide }) : null })) : null, 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 ?? "" })] }));
|
|
1475
1537
|
}
|
|
1476
1538
|
/** One instanced unit-box mesh from precomposed matrices (bridge parts). */
|
|
1477
1539
|
function InstancedBoxes({ matrices, color }) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CityIntersection, CityStreet } from "@jgengine/core/world/cityKind";
|
|
2
|
+
/**
|
|
3
|
+
* Surface for a crossing's welded patch: gravel only when every street running through the crossing
|
|
4
|
+
* is gravel, so an unpaved hamlet's junctions stay gravel while a lane meeting a paved main stays asphalt.
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export declare function junctionSurface(cross: Pick<CityIntersection, "x" | "z" | "radius">, streets: readonly Pick<CityStreet, "points" | "width" | "surface">[]): CityStreet["surface"];
|
|
@@ -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,
|
package/dist/terrain/index.d.ts
CHANGED
|
@@ -10,3 +10,7 @@ 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";
|