@jgengine/shell 0.21.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.
Files changed (102) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/dist/GamePlayerShell.js +18 -3
  3. package/dist/GameUiPreview.js +2 -1
  4. package/dist/Shell3dPresentation.js +13 -8
  5. package/dist/ShellChrome.d.ts +1 -1
  6. package/dist/ShellChrome.js +4 -2
  7. package/dist/ShellHudPresentation.js +2 -1
  8. package/dist/ShellNotices.d.ts +26 -0
  9. package/dist/ShellNotices.js +51 -0
  10. package/dist/audio/AudioComponents.d.ts +3 -1
  11. package/dist/audio/AudioComponents.js +5 -2
  12. package/dist/audio/listenerAnchor.d.ts +15 -0
  13. package/dist/audio/listenerAnchor.js +17 -0
  14. package/dist/audio/playableAudio.d.ts +9 -0
  15. package/dist/audio/playableAudio.js +11 -0
  16. package/dist/commandSink.d.ts +1 -1
  17. package/dist/commandSink.js +8 -3
  18. package/dist/defineGame.js +4 -2
  19. package/dist/devtools/DevtoolsOverlay.js +1 -0
  20. package/dist/devtools/agentBridge.d.ts +21 -0
  21. package/dist/devtools/agentBridge.js +95 -3
  22. package/dist/devtools/devtoolsReports.d.ts +32 -0
  23. package/dist/devtools/devtoolsReports.js +26 -0
  24. package/dist/devtools/simStepper.d.ts +21 -0
  25. package/dist/devtools/simStepper.js +37 -0
  26. package/dist/drivers/FrameDriver.js +26 -14
  27. package/dist/drivers/HudOnlyDriver.js +16 -5
  28. package/dist/environment/EnvironmentScene.js +24 -9
  29. package/dist/environment/RoadJunctions.d.ts +3 -1
  30. package/dist/environment/RoadJunctions.js +9 -2
  31. package/dist/environment/RoadRibbons.d.ts +3 -1
  32. package/dist/environment/RoadRibbons.js +16 -3
  33. package/dist/environment/terrainGroundColor.d.ts +5 -4
  34. package/dist/environment/terrainGroundColor.js +20 -9
  35. package/dist/materialOverride.js +4 -2
  36. package/dist/playSurfaceFocus.d.ts +16 -0
  37. package/dist/playSurfaceFocus.js +42 -0
  38. package/dist/postfx/PostProcessing.d.ts +3 -1
  39. package/dist/postfx/PostProcessing.js +14 -6
  40. package/dist/registry.d.ts +5 -1
  41. package/dist/render/CascadedShadows.d.ts +2 -0
  42. package/dist/render/CascadedShadows.js +4 -0
  43. package/dist/render/EntityPreview.js +1 -1
  44. package/dist/render/EnvironmentLighting.d.ts +26 -7
  45. package/dist/render/EnvironmentLighting.js +40 -27
  46. package/dist/render/SceneLighting.js +2 -2
  47. package/dist/render/modelLoad.d.ts +7 -0
  48. package/dist/render/modelLoad.js +13 -0
  49. package/dist/render/modelRender.d.ts +1 -0
  50. package/dist/render/modelRender.js +3 -0
  51. package/dist/render/modelSurface.d.ts +39 -0
  52. package/dist/render/modelSurface.js +206 -0
  53. package/dist/render/useModelInstance.d.ts +1 -1
  54. package/dist/render/useModelInstance.js +2 -2
  55. package/dist/scatter/scatterModels.js +3 -1
  56. package/dist/scene/AuthoredScene.d.ts +11 -3
  57. package/dist/scene/AuthoredScene.js +17 -9
  58. package/dist/scene/CityRenderer.js +86 -24
  59. package/dist/scene/GrassFieldRenderer.js +20 -2
  60. package/dist/scene/cityJunctionSurface.d.ts +7 -0
  61. package/dist/scene/cityJunctionSurface.js +34 -0
  62. package/dist/settings/settingsController.js +1 -1
  63. package/dist/terrain/CarvedTerrain.d.ts +2 -1
  64. package/dist/terrain/CarvedTerrain.js +2 -2
  65. package/dist/terrain/GrassField.d.ts +2 -1
  66. package/dist/terrain/GrassField.js +3 -2
  67. package/dist/terrain/grassGeometry.d.ts +3 -0
  68. package/dist/terrain/grassGeometry.js +2 -1
  69. package/dist/terrain/index.d.ts +5 -0
  70. package/dist/terrain/index.js +5 -0
  71. package/dist/terrain/surfaceRibbonMaterial.d.ts +62 -0
  72. package/dist/terrain/surfaceRibbonMaterial.js +227 -0
  73. package/dist/terrain/terrainDetailMaterial.d.ts +33 -10
  74. package/dist/terrain/terrainDetailMaterial.js +149 -63
  75. package/dist/terrain/terrainMath.d.ts +2 -0
  76. package/dist/terrain/terrainMath.js +2 -0
  77. package/dist/terrain/terrainPaintColor.d.ts +20 -0
  78. package/dist/terrain/terrainPaintColor.js +92 -0
  79. package/dist/terrain/terrainQuality.d.ts +5 -0
  80. package/dist/terrain/terrainQuality.js +7 -0
  81. package/dist/terrain/terrainSurfaceShader.d.ts +16 -0
  82. package/dist/terrain/terrainSurfaceShader.js +266 -0
  83. package/dist/terrain/terrainSurfaces.d.ts +59 -0
  84. package/dist/terrain/terrainSurfaces.js +199 -0
  85. package/dist/world/StaticObjectModels.d.ts +1 -1
  86. package/dist/world/StaticObjectModels.js +1 -1
  87. package/dist/world/WorldScene.js +1 -1
  88. package/dist/world/staticObjectBatches.d.ts +5 -1
  89. package/dist/world/staticObjectBatches.js +9 -5
  90. package/dist/world/worldBarSamples.d.ts +13 -1
  91. package/dist/world/worldBarSamples.js +40 -8
  92. package/package.json +5 -5
  93. package/skills/game-design/SKILL.md +2 -0
  94. package/skills/jgengine/SKILL.md +1 -0
  95. package/skills/jgengine/api.md +18 -18
  96. package/skills/jgengine-ui/SKILL.md +4 -0
  97. package/skills/jgengine-ui/api.md +242 -34
  98. package/skills/jgengine-ui/capabilities.md +56 -0
  99. package/skills/jgengine-ui/reference-primitives.md +4 -4
  100. package/skills/jgengine-world/api.md +28 -15
  101. package/skills/jgengine-world/capabilities.md +4 -0
  102. package/skills/jgengine-world/reference.md +30 -0
@@ -0,0 +1,266 @@
1
+ /** Integer-hash value noise (`jgHashUint`, `jgVNoise`, `jgFbm`) every surface chunk builds on. */
2
+ export const NOISE_GLSL = /* glsl */ `
3
+ uint jgHashUint(uvec2 v){
4
+ uint x = v.x * 1664525u + v.y * 1013904223u;
5
+ x = (x ^ (x >> 16u)) * 2246822519u;
6
+ x = (x ^ (x >> 13u)) * 3266489917u;
7
+ x = x ^ (x >> 16u);
8
+ return x;
9
+ }
10
+ float jgHash(vec2 p){
11
+ ivec2 i = ivec2(floor(p));
12
+ return float(jgHashUint(uvec2(i))) * (1.0 / 4294967296.0);
13
+ }
14
+ float jgVNoise(vec2 p){
15
+ vec2 i = floor(p); vec2 f = fract(p);
16
+ vec2 u = f * f * (3.0 - 2.0 * f);
17
+ float a = jgHash(i);
18
+ float b = jgHash(i + vec2(1.0, 0.0));
19
+ float c = jgHash(i + vec2(0.0, 1.0));
20
+ float d = jgHash(i + vec2(1.0, 1.0));
21
+ return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
22
+ }
23
+ float jgFbm(vec2 p){
24
+ float v = 0.0; float amp = 0.5;
25
+ for (int i = 0; i < 4; i++){ v += amp * jgVNoise(p); p *= 2.0; amp *= 0.5; }
26
+ return v;
27
+ }
28
+ `;
29
+ /**
30
+ * GLSL for the terrain surface pass: per-surface procedural ground (grass, dirt, gravel, rock, sand,
31
+ * mud, snow, road) and the height-aware blend between painted layers. Needs `NOISE_GLSL` first.
32
+ * Every octave is gated by `jgBand(frequency)` so detail finer than a pixel fades to its mean
33
+ * instead of shimmering at distance.
34
+ */
35
+ export const SURFACE_FUNCTIONS_GLSL = /* glsl */ `
36
+ float jgFootprint;
37
+ float jgFbm2(vec2 p){ return jgVNoise(p) * 0.62 + jgVNoise(p * 2.03 + 17.3) * 0.38; }
38
+ float jgFbm3o(vec2 p){ return (jgVNoise(p) * 0.5 + jgVNoise(p * 2.03 + 17.3) * 0.3 + jgVNoise(p * 4.1 + 5.9) * 0.2); }
39
+ float jgBand(float freq){ return 1.0 - smoothstep(0.16, 0.42, freq * jgFootprint); }
40
+ vec2 jgHash22(vec2 cell){
41
+ uint h = jgHashUint(uvec2(ivec2(floor(cell + 0.5))));
42
+ uint h2 = h * 747796405u + 2891336453u;
43
+ h2 = (h2 ^ (h2 >> 15u)) * 2246822519u;
44
+ return vec2(float(h), float(h2 ^ (h2 >> 13u))) * (1.0 / 4294967296.0);
45
+ }
46
+ float jgHash3(vec3 i){
47
+ ivec3 c = ivec3(i);
48
+ return float(jgHashUint(uvec2(uint(c.x) ^ (uint(c.z) * 2654435761u), uint(c.y) + uint(c.z) * 1013904223u))) * (1.0 / 4294967296.0);
49
+ }
50
+ float jgVNoise3(vec3 p){
51
+ vec3 i = floor(p); vec3 f = fract(p);
52
+ vec3 u = f * f * (3.0 - 2.0 * f);
53
+ float a = jgHash3(i), b = jgHash3(i + vec3(1.0, 0.0, 0.0));
54
+ float c = jgHash3(i + vec3(0.0, 1.0, 0.0)), d = jgHash3(i + vec3(1.0, 1.0, 0.0));
55
+ float e = jgHash3(i + vec3(0.0, 0.0, 1.0)), g = jgHash3(i + vec3(1.0, 0.0, 1.0));
56
+ float h = jgHash3(i + vec3(0.0, 1.0, 1.0)), k = jgHash3(i + vec3(1.0, 1.0, 1.0));
57
+ return mix(mix(mix(a, b, u.x), mix(c, d, u.x), u.y), mix(mix(e, g, u.x), mix(h, k, u.x), u.y), u.z);
58
+ }
59
+ float jgFbm3(vec3 p){
60
+ float v = 0.0; float amp = 0.5;
61
+ for (int i = 0; i < 3; i++){ v += amp * jgVNoise3(p); p = p * 2.03 + vec3(1.7, 9.2, 4.1); amp *= 0.5; }
62
+ return v / 0.875;
63
+ }
64
+ // x: distance to the nearest feature point, y: F2 - F1 (0 on a cell border), z: that cell's random id.
65
+ vec3 jgVoronoi(vec2 p){
66
+ vec2 n = floor(p); vec2 f = fract(p);
67
+ float d1 = 8.0; float d2 = 8.0; float id = 0.0;
68
+ for (int j = -1; j <= 1; j++){
69
+ for (int i = -1; i <= 1; i++){
70
+ vec2 g = vec2(float(i), float(j));
71
+ vec2 o = jgHash22(n + g);
72
+ vec2 r = g + 0.15 + o * 0.7 - f;
73
+ float d = dot(r, r);
74
+ if (d < d1){ d2 = d1; d1 = d; id = o.x; } else if (d < d2){ d2 = d; }
75
+ }
76
+ }
77
+ d1 = sqrt(d1);
78
+ return vec3(d1, sqrt(d2) - d1, id);
79
+ }
80
+
81
+ struct JgSurf { vec3 albedo; float height; float rough; float bump; };
82
+
83
+ // height: 0..1 relief used to decide which layer wins along a blend edge; bump: displacement in metres.
84
+ JgSurf jgSurface(int cls, vec3 color, float rough, float scale, vec3 wp){
85
+ vec2 p = wp.xz * scale;
86
+ JgSurf s;
87
+ s.albedo = color; s.rough = rough; s.height = 0.4; s.bump = 0.0;
88
+ float m = jgFbm3o(p / 1.6 + float(cls) * 13.7);
89
+ #ifdef JG_SURFACE_LOW
90
+ s.albedo = color * (0.84 + 0.34 * m);
91
+ s.height = m;
92
+ return s;
93
+ #else
94
+ bool shaded = false;
95
+ #if (JG_SURFACE_CLASSES & 2) != 0
96
+ if (cls == 1) {
97
+ float clump = jgFbm3o(p / 0.9 + 5.3);
98
+ float dry = smoothstep(0.5, 0.75, jgFbm2(p / 7.0 + 17.0));
99
+ float blade = jgVNoise(p * 11.0 + clump * 3.0);
100
+ float fine = jgVNoise(p * 27.0 + 91.0);
101
+ float bBlade = jgBand(11.0);
102
+ s.albedo = mix(color, color * vec3(1.24, 1.12, 0.7), dry * 0.55);
103
+ s.albedo *= 0.74 + 0.48 * clump;
104
+ s.albedo = mix(s.albedo, color * vec3(0.78, 0.64, 0.46), smoothstep(0.42, 0.24, clump) * 0.25);
105
+ s.albedo *= 1.0 + (blade - 0.5) * 0.5 * bBlade + (fine - 0.5) * 0.32 * jgBand(27.0);
106
+ s.height = 0.2 + clump * 0.55 + blade * 0.25 * bBlade;
107
+ s.bump = clump * 0.014 + blade * 0.008 * bBlade;
108
+ shaded = true;
109
+ }
110
+ #endif
111
+ #if (JG_SURFACE_CLASSES & 4) != 0
112
+ if (cls == 2) {
113
+ float clod = jgVNoise(p * 3.0 + m * 2.0);
114
+ float crumb = jgVNoise(p * 14.0 + 7.0);
115
+ vec3 v = jgVoronoi(p * 2.2 + 7.7);
116
+ float peb = (1.0 - smoothstep(0.1, 0.2, v.x)) * step(0.7, v.z) * jgBand(7.0);
117
+ s.albedo = color * (0.78 + 0.44 * m);
118
+ s.albedo *= 1.0 + (clod - 0.5) * 0.32 * jgBand(3.0) + (crumb - 0.5) * 0.24 * jgBand(14.0);
119
+ s.albedo = mix(s.albedo, color * vec3(1.34, 1.3, 1.24) + 0.02, peb * 0.75);
120
+ s.height = 0.05 + m * 0.4 + clod * 0.2 + peb * 0.5;
121
+ s.bump = clod * 0.012 * jgBand(3.0) + crumb * 0.003 * jgBand(14.0) + peb * 0.012;
122
+ shaded = true;
123
+ }
124
+ #endif
125
+ #if (JG_SURFACE_CLASSES & 8) != 0
126
+ if (cls == 3) {
127
+ // Round pebbles from F1 distance (not Voronoi cells, which read as flagstone paving), two sizes,
128
+ // packed over darker grit.
129
+ vec3 va = jgVoronoi(p * 5.5);
130
+ float ra = 0.28 + 0.18 * fract(va.z * 13.1);
131
+ float sa = 1.0 - smoothstep(ra * 0.72, ra, va.x);
132
+ float da = sqrt(max(0.0, 1.0 - (va.x / ra) * (va.x / ra))) * sa;
133
+ vec3 vb = jgVoronoi(p * 11.0 + 3.1);
134
+ float rb = 0.32 + 0.14 * fract(vb.z * 7.7);
135
+ float sb = (1.0 - smoothstep(rb * 0.66, rb, vb.x)) * (1.0 - sa);
136
+ float db = sqrt(max(0.0, 1.0 - (vb.x / rb) * (vb.x / rb))) * sb;
137
+ float grit = jgVNoise(p * 24.0);
138
+ float bA = jgBand(5.5);
139
+ float bB = jgBand(11.0);
140
+ vec3 hueA = mix(vec3(1.08, 1.0, 0.9), vec3(0.92, 0.97, 1.08), fract(va.z * 7.13));
141
+ vec3 hueB = mix(vec3(1.05, 1.0, 0.93), vec3(0.95, 0.98, 1.05), fract(vb.z * 5.71));
142
+ vec3 near = color * (0.58 + 0.2 * grit);
143
+ near = mix(near, color * hueB * (0.66 + 0.5 * vb.z) * (0.72 + 0.28 * db), sb * bB);
144
+ near = mix(near, color * hueA * (0.64 + 0.62 * va.z) * (0.7 + 0.3 * da), sa);
145
+ s.albedo = mix(color * (0.8 + 0.26 * m), near, bA);
146
+ s.height = 0.3 + max(da, db * 0.6 * bB) * 0.6 * bA;
147
+ s.bump = (da * 0.03 + db * 0.012 * bB) * bA;
148
+ s.rough = rough - 0.08 * sa * bA;
149
+ shaded = true;
150
+ }
151
+ #endif
152
+ #if (JG_SURFACE_CLASSES & 16) != 0
153
+ if (cls == 4) {
154
+ vec3 q = wp * scale;
155
+ float n = jgFbm3(q / 1.8);
156
+ float n2 = jgFbm3(q * 1.7 + 5.0);
157
+ float grain = jgVNoise3(q * 9.0);
158
+ float strata = sin(q.y * 2.6 + n * 5.0) * 0.5 + 0.5;
159
+ float pits = smoothstep(0.72, 0.9, jgVNoise3(q * 4.3 + 11.0)) * jgBand(4.3);
160
+ float lichen = smoothstep(0.6, 0.78, jgFbm2(p / 3.0 + 41.0));
161
+ s.albedo = color * (0.4 + 1.25 * n) * (0.8 + 0.4 * n2 * jgBand(3.4)) * (1.0 - pits * 0.35) * (0.94 + strata * 0.12);
162
+ s.albedo *= 1.0 + (grain - 0.5) * 0.3 * jgBand(9.0);
163
+ s.albedo = mix(s.albedo, s.albedo * vec3(0.92, 1.06, 0.82), lichen * 0.45);
164
+ s.height = 0.35 + n * 0.65;
165
+ s.bump = n * 0.1 + n2 * 0.02 * jgBand(3.4) - pits * 0.006 + grain * 0.004 * jgBand(9.0);
166
+ s.rough = rough - 0.1 * n;
167
+ shaded = true;
168
+ }
169
+ #endif
170
+ #if (JG_SURFACE_CLASSES & 32) != 0
171
+ if (cls == 5) {
172
+ float warp = jgFbm2(p / 3.0 + 3.3);
173
+ float ripple = sin((p.x * 0.83 + p.y * 0.56) * 6.0 + warp * 7.0) * 0.5 + 0.5;
174
+ float grain = jgVNoise(p * 40.0);
175
+ float bRipple = jgBand(1.0);
176
+ s.albedo = color * (0.92 + 0.16 * m) * (1.0 + (ripple - 0.5) * 0.1 * bRipple);
177
+ float speck = jgVNoise(p * 9.0 + 2.7);
178
+ s.albedo *= 1.0 + (grain - 0.5) * 0.14 * jgBand(40.0) + (speck - 0.5) * 0.12 * jgBand(9.0);
179
+ s.height = 0.05 + ripple * 0.25 + m * 0.2;
180
+ s.bump = ripple * 0.012 * bRipple;
181
+ shaded = true;
182
+ }
183
+ #endif
184
+ #if (JG_SURFACE_CLASSES & 64) != 0
185
+ if (cls == 6) {
186
+ float puddle = smoothstep(0.56, 0.66, jgFbm3o(p / 3.2 + 31.0));
187
+ float lumps = jgVNoise(p * 4.0 + m);
188
+ s.albedo = color * (0.8 + 0.38 * m) * (1.0 + (lumps - 0.5) * 0.26 * jgBand(4.0));
189
+ s.albedo = mix(s.albedo, s.albedo * 0.58, puddle);
190
+ s.rough = mix(rough, 0.22, puddle);
191
+ s.height = (0.1 + m * 0.4 + lumps * 0.2) * (1.0 - puddle * 0.8);
192
+ s.bump = lumps * 0.012 * jgBand(4.0) * (1.0 - puddle);
193
+ shaded = true;
194
+ }
195
+ #endif
196
+ #if (JG_SURFACE_CLASSES & 128) != 0
197
+ if (cls == 7) {
198
+ float drift = jgFbm2(p / 4.0 + 9.0);
199
+ s.albedo = color * (0.93 + 0.1 * m + 0.04 * drift);
200
+ s.height = 0.3 + drift * 0.5;
201
+ s.bump = drift * 0.03 + m * 0.01;
202
+ shaded = true;
203
+ }
204
+ #endif
205
+ #if (JG_SURFACE_CLASSES & 256) != 0
206
+ if (cls == 8) {
207
+ float grain = jgVNoise(p * 30.0);
208
+ float stain = jgFbm2(p / 5.0 + 2.0);
209
+ s.albedo = color * (0.88 + 0.22 * stain) * (1.0 + (grain - 0.5) * 0.2 * jgBand(30.0));
210
+ s.height = 0.2 + stain * 0.2;
211
+ s.bump = grain * 0.0015 * jgBand(30.0);
212
+ shaded = true;
213
+ }
214
+ #endif
215
+ if (!shaded) {
216
+ float fine = jgVNoise(p * 6.0 + 3.0);
217
+ s.albedo = color * (1.0 + (fine - 0.5) * 0.18 * jgBand(6.0));
218
+ s.height = 0.25 + m * 0.4;
219
+ s.bump = m * 0.02 + fine * 0.004 * jgBand(6.0);
220
+ }
221
+ return s;
222
+ #endif
223
+ }
224
+ `;
225
+ /** Paint-coverage uniforms and the warped, smoothed cell lookup. */
226
+ export const PAINT_PARS_GLSL = /* glsl */ `
227
+ uniform sampler2D uPaintA;
228
+ uniform sampler2D uPaintB;
229
+ uniform vec2 uPaintMin;
230
+ uniform vec2 uPaintCell;
231
+ uniform vec2 uPaintGrid;
232
+ uniform float uPaintWarp;
233
+ uniform vec3 uLayerColor[8];
234
+ uniform float uLayerClass[8];
235
+ uniform float uLayerRough[8];
236
+ uniform float uLayerScale[8];
237
+ float jgWarpNoise(vec2 p){ return (jgVNoise(p) * 0.65 + jgVNoise(p * 2.7 + 13.1) * 0.35) - 0.5; }
238
+ vec2 jgPaintUv(vec2 wp){
239
+ vec2 cell = (wp - uPaintMin) / uPaintCell;
240
+ cell += vec2(jgWarpNoise(wp / 2.3 + 4.1), jgWarpNoise(wp / 2.3 + vec2(19.7, 3.3))) * uPaintWarp;
241
+ vec2 c = cell - 0.5;
242
+ vec2 i = floor(c); vec2 f = fract(c);
243
+ f = f * f * (3.0 - 2.0 * f);
244
+ return (i + f + 0.5) / uPaintGrid;
245
+ }
246
+ `;
247
+ /**
248
+ * Micro-normal from a surface height `jgBumpH` (metres) via screen-space derivatives; goes after
249
+ * `normal_fragment_maps`. Skipped on the low tier.
250
+ */
251
+ export const SURFACE_BUMP_NORMAL_GLSL = /* glsl */ `
252
+ #ifndef JG_SURFACE_LOW
253
+ {
254
+ // Bump from the blended surface height (metres) via screen-space derivatives, unnormalised so the
255
+ // tilt is the true world slope of that height field at any distance.
256
+ vec3 jgSigX = dFdx(-vViewPosition);
257
+ vec3 jgSigY = dFdy(-vViewPosition);
258
+ vec2 jgDh = vec2(dFdx(jgBumpH), dFdy(jgBumpH));
259
+ vec3 jgR1 = cross(jgSigY, normal);
260
+ vec3 jgR2 = cross(normal, jgSigX);
261
+ float jgDet = dot(jgSigX, jgR1) * faceDirection;
262
+ vec3 jgGrad = sign(jgDet) * (jgDh.x * jgR1 + jgDh.y * jgR2);
263
+ normal = normalize(abs(jgDet) * normal - jgGrad);
264
+ }
265
+ #endif
266
+ `;
@@ -0,0 +1,59 @@
1
+ import * as THREE from "three";
2
+ import type { TerrainPaintSource } from "./terrainPaintColor.js";
3
+ /** Procedural ground response the terrain shader renders for a painted surface id. */
4
+ export type TerrainSurfaceClass = "ground" | "grass" | "dirt" | "gravel" | "rock" | "sand" | "mud" | "snow" | "road";
5
+ /**
6
+ * Classifies a painted surface id (e.g. `"grass"`, `"dry-grass"`, `"cobble-stone"`) into the procedural
7
+ * ground response the terrain shader draws for it; unknown ids get the generic `"ground"` response.
8
+ */
9
+ export declare function terrainSurfaceClass(surface: string): TerrainSurfaceClass;
10
+ /** Shader index of a surface class (`jgSurface`'s `cls`); bit `1 << index` in a class mask. */
11
+ export declare function terrainSurfaceClassIndex(cls: TerrainSurfaceClass): number;
12
+ /** Default roughness the terrain shades a surface class with. */
13
+ export declare function terrainSurfaceRoughness(cls: TerrainSurfaceClass): number;
14
+ /** At most this many distinct paint layers reach the GPU; extra layers fold into the last slot. */
15
+ export declare const MAX_TERRAIN_PAINT_LAYERS = 8;
16
+ /** One GPU paint slot: authored hue plus the procedural response it renders with. */
17
+ export interface TerrainPaintSlot {
18
+ color: THREE.Color;
19
+ surfaceClass: TerrainSurfaceClass;
20
+ classIndex: number;
21
+ roughness: number;
22
+ /** Pattern frequency multiplier from the layer's authored `tiling`. */
23
+ scale: number;
24
+ }
25
+ /** Per-cell paint coverage packed for the shader: two RGBA8 planes holding up to eight layer weights. */
26
+ export interface TerrainPaintWeights {
27
+ cols: number;
28
+ rows: number;
29
+ bounds: {
30
+ minX: number;
31
+ minZ: number;
32
+ maxX: number;
33
+ maxZ: number;
34
+ };
35
+ slots: TerrainPaintSlot[];
36
+ /** `cols*rows*4` bytes each: slots 0–3 and 4–7, 0..255 coverage scaled by layer opacity. */
37
+ planes: [Uint8Array, Uint8Array];
38
+ }
39
+ /**
40
+ * Packs a terrain's authored paint (layer stack, blend weights or hard surface ids) into per-cell
41
+ * coverage planes for the terrain surface shader. Returns `undefined` when the source exposes no
42
+ * cell grid. Pass `into` to rewrite an existing buffer in place after a paint stroke.
43
+ */
44
+ export declare function packTerrainPaintWeights(source: TerrainPaintSource, into?: TerrainPaintWeights): TerrainPaintWeights | undefined;
45
+ /** GPU-side paint coverage shared by the terrain surface material; `update` after each paint stroke. */
46
+ export interface TerrainPaintTextures {
47
+ /** Shader uniforms (`uPaintA`, `uLayerColor`, …) to merge into the surface material. */
48
+ readonly uniforms: Record<string, THREE.IUniform>;
49
+ /** Bit `1 << classIndex` per surface class the current slots use; shaders compile only these responses. */
50
+ readonly classMask: number;
51
+ /** Re-upload coverage and layer slots; reallocates only when the cell grid size changes. */
52
+ update(weights: TerrainPaintWeights): void;
53
+ dispose(): void;
54
+ }
55
+ /**
56
+ * Uploads packed paint coverage ({@link packTerrainPaintWeights}) as two linear-filtered RGBA
57
+ * textures plus per-slot hue, surface class, roughness and pattern scale for the terrain surface material.
58
+ */
59
+ export declare function createTerrainPaintTextures(weights: TerrainPaintWeights): TerrainPaintTextures;
@@ -0,0 +1,199 @@
1
+ import * as THREE from "three";
2
+ import { TERRAIN_MATERIAL_COLORS } from "@jgengine/core/editor/terrainMaterials";
3
+ const CLASS_INDEX = {
4
+ ground: 0, grass: 1, dirt: 2, gravel: 3, rock: 4, sand: 5, mud: 6, snow: 7, road: 8,
5
+ };
6
+ /** Authored-tiling neutral point per class: a layer's `tiling` divided by this scales the pattern. */
7
+ const CLASS_TILING = {
8
+ ground: 3, grass: 3, dirt: 2, gravel: 2, rock: 4, sand: 3, mud: 2, snow: 3, road: 2,
9
+ };
10
+ const CLASS_ROUGHNESS = {
11
+ ground: 0.92, grass: 0.95, dirt: 0.93, gravel: 0.88, rock: 0.82, sand: 0.96, mud: 0.8, snow: 0.78, road: 0.86,
12
+ };
13
+ const ALIASES = [
14
+ ["gravel", "gravel"], ["pebble", "gravel"], ["shingle", "gravel"], ["scree", "gravel"],
15
+ ["grass", "grass"], ["moss", "grass"], ["meadow", "grass"], ["lawn", "grass"], ["turf", "grass"], ["leaf", "grass"],
16
+ ["mud", "mud"], ["clay", "mud"], ["swamp", "mud"], ["bog", "mud"],
17
+ ["sand", "sand"], ["beach", "sand"], ["dune", "sand"],
18
+ ["snow", "snow"], ["ice", "snow"], ["frost", "snow"],
19
+ ["road", "road"], ["asphalt", "road"], ["tarmac", "road"], ["concrete", "road"], ["pavement", "road"],
20
+ ["rock", "rock"], ["stone", "rock"], ["cliff", "rock"], ["slate", "rock"], ["basalt", "rock"],
21
+ ["dirt", "dirt"], ["soil", "dirt"], ["earth", "dirt"], ["path", "dirt"], ["trail", "dirt"], ["ash", "dirt"],
22
+ ];
23
+ /**
24
+ * Classifies a painted surface id (e.g. `"grass"`, `"dry-grass"`, `"cobble-stone"`) into the procedural
25
+ * ground response the terrain shader draws for it; unknown ids get the generic `"ground"` response.
26
+ */
27
+ export function terrainSurfaceClass(surface) {
28
+ const id = surface.toLowerCase();
29
+ for (const [key, cls] of ALIASES)
30
+ if (id === key)
31
+ return cls;
32
+ for (const [key, cls] of ALIASES)
33
+ if (id.includes(key))
34
+ return cls;
35
+ return "ground";
36
+ }
37
+ /** Shader index of a surface class (`jgSurface`'s `cls`); bit `1 << index` in a class mask. */
38
+ export function terrainSurfaceClassIndex(cls) {
39
+ return CLASS_INDEX[cls];
40
+ }
41
+ /** Default roughness the terrain shades a surface class with. */
42
+ export function terrainSurfaceRoughness(cls) {
43
+ return CLASS_ROUGHNESS[cls];
44
+ }
45
+ /** At most this many distinct paint layers reach the GPU; extra layers fold into the last slot. */
46
+ export const MAX_TERRAIN_PAINT_LAYERS = 8;
47
+ function slotFor(surface, layer) {
48
+ const surfaceClass = terrainSurfaceClass(surface);
49
+ const tiling = layer?.tiling;
50
+ return {
51
+ color: new THREE.Color(layer?.tint ?? TERRAIN_MATERIAL_COLORS[surface] ?? "#7c7f86"),
52
+ surfaceClass,
53
+ classIndex: CLASS_INDEX[surfaceClass],
54
+ roughness: layer?.roughness ?? CLASS_ROUGHNESS[surfaceClass],
55
+ scale: tiling === undefined || tiling <= 0 ? 1 : Math.min(2, Math.max(0.5, CLASS_TILING[surfaceClass] / tiling)),
56
+ };
57
+ }
58
+ /**
59
+ * Packs a terrain's authored paint (layer stack, blend weights or hard surface ids) into per-cell
60
+ * coverage planes for the terrain surface shader. Returns `undefined` when the source exposes no
61
+ * cell grid. Pass `into` to rewrite an existing buffer in place after a paint stroke.
62
+ */
63
+ export function packTerrainPaintWeights(source, into) {
64
+ const { gridBounds, cols, rows } = source;
65
+ if (gridBounds === undefined || cols === undefined || rows === undefined || cols < 1 || rows < 1)
66
+ return undefined;
67
+ const layers = source.layers;
68
+ const slotIndex = new Map();
69
+ const slots = [];
70
+ const addSlot = (key, surface, layer) => {
71
+ const known = slotIndex.get(key);
72
+ if (known !== undefined)
73
+ return known;
74
+ const index = Math.min(slots.length, MAX_TERRAIN_PAINT_LAYERS - 1);
75
+ if (slots.length < MAX_TERRAIN_PAINT_LAYERS)
76
+ slots.push(slotFor(surface, layer));
77
+ slotIndex.set(key, index);
78
+ return index;
79
+ };
80
+ const layerSlots = layers.map(layer => addSlot(`layer:${layer.id}`, layer.surface, layer));
81
+ const surfaceSlot = (surface) => {
82
+ const index = layers.findIndex(layer => layer.surface === surface);
83
+ return index >= 0 ? layerSlots[index] : addSlot(`surface:${surface}`, surface, undefined);
84
+ };
85
+ const size = cols * rows * 4;
86
+ const planes = into !== undefined && into.cols === cols && into.rows === rows ? into.planes : [new Uint8Array(size), new Uint8Array(size)];
87
+ planes[0].fill(0);
88
+ planes[1].fill(0);
89
+ const cellW = (gridBounds.maxX - gridBounds.minX) / cols;
90
+ const cellD = (gridBounds.maxZ - gridBounds.minZ) / rows;
91
+ const accum = new Float32Array(MAX_TERRAIN_PAINT_LAYERS);
92
+ for (let gz = 0; gz < rows; gz++) {
93
+ const z = gridBounds.minZ + (gz + 0.5) * cellD;
94
+ for (let gx = 0; gx < cols; gx++) {
95
+ const x = gridBounds.minX + (gx + 0.5) * cellW;
96
+ accum.fill(0);
97
+ const weights = source.weightsAt(x, z);
98
+ if (layers.length > 0 && weights.length === layers.length) {
99
+ for (let i = 0; i < layers.length; i++)
100
+ accum[layerSlots[i]] += weights[i] * (layers[i].opacity ?? 1);
101
+ }
102
+ else {
103
+ const surface = source.surfaceAt(x, z);
104
+ if (surface === null)
105
+ continue;
106
+ const slot = surfaceSlot(surface);
107
+ const layer = layers.find(candidate => candidate.surface === surface);
108
+ accum[slot] = layer?.opacity ?? 1;
109
+ }
110
+ const cell = (gz * cols + gx) * 4;
111
+ for (let i = 0; i < MAX_TERRAIN_PAINT_LAYERS; i++) {
112
+ const value = Math.round(Math.min(1, Math.max(0, accum[i])) * 255);
113
+ if (value === 0)
114
+ continue;
115
+ planes[i < 4 ? 0 : 1][cell + (i & 3)] = value;
116
+ }
117
+ }
118
+ }
119
+ return {
120
+ cols,
121
+ rows,
122
+ bounds: { minX: gridBounds.minX, minZ: gridBounds.minZ, maxX: gridBounds.maxX, maxZ: gridBounds.maxZ },
123
+ slots,
124
+ planes,
125
+ };
126
+ }
127
+ /** Edge warp, in cells: how far noise pushes a painted boundary off the cell grid. */
128
+ const PAINT_EDGE_WARP = 1.5;
129
+ function coverageTexture(data, cols, rows) {
130
+ const texture = new THREE.DataTexture(data, cols, rows, THREE.RGBAFormat, THREE.UnsignedByteType);
131
+ texture.magFilter = THREE.LinearFilter;
132
+ texture.minFilter = THREE.LinearFilter;
133
+ texture.wrapS = THREE.ClampToEdgeWrapping;
134
+ texture.wrapT = THREE.ClampToEdgeWrapping;
135
+ texture.generateMipmaps = false;
136
+ texture.needsUpdate = true;
137
+ return texture;
138
+ }
139
+ /**
140
+ * Uploads packed paint coverage ({@link packTerrainPaintWeights}) as two linear-filtered RGBA
141
+ * textures plus per-slot hue, surface class, roughness and pattern scale for the terrain surface material.
142
+ */
143
+ export function createTerrainPaintTextures(weights) {
144
+ const colors = Array.from({ length: MAX_TERRAIN_PAINT_LAYERS }, () => new THREE.Color());
145
+ const classes = new Float32Array(MAX_TERRAIN_PAINT_LAYERS);
146
+ const roughness = new Float32Array(MAX_TERRAIN_PAINT_LAYERS);
147
+ const scales = new Float32Array(MAX_TERRAIN_PAINT_LAYERS);
148
+ const uniforms = {
149
+ uPaintA: { value: coverageTexture(weights.planes[0], weights.cols, weights.rows) },
150
+ uPaintB: { value: coverageTexture(weights.planes[1], weights.cols, weights.rows) },
151
+ uPaintMin: { value: new THREE.Vector2() },
152
+ uPaintCell: { value: new THREE.Vector2() },
153
+ uPaintGrid: { value: new THREE.Vector2() },
154
+ uPaintWarp: { value: PAINT_EDGE_WARP },
155
+ uLayerColor: { value: colors },
156
+ uLayerClass: { value: classes },
157
+ uLayerRough: { value: roughness },
158
+ uLayerScale: { value: scales },
159
+ };
160
+ let classMask = 0;
161
+ const update = (next) => {
162
+ classMask = next.slots.reduce((mask, slot) => mask | (1 << slot.classIndex), 0);
163
+ for (const [key, plane] of [["uPaintA", next.planes[0]], ["uPaintB", next.planes[1]]]) {
164
+ const current = uniforms[key].value;
165
+ if (current.image.width === next.cols && current.image.height === next.rows) {
166
+ if (current.image.data !== plane)
167
+ current.image.data.set(plane);
168
+ current.needsUpdate = true;
169
+ }
170
+ else {
171
+ current.dispose();
172
+ uniforms[key].value = coverageTexture(plane, next.cols, next.rows);
173
+ }
174
+ }
175
+ const { bounds, cols, rows } = next;
176
+ uniforms.uPaintMin.value.set(bounds.minX, bounds.minZ);
177
+ uniforms.uPaintCell.value.set((bounds.maxX - bounds.minX) / cols, (bounds.maxZ - bounds.minZ) / rows);
178
+ uniforms.uPaintGrid.value.set(cols, rows);
179
+ for (let i = 0; i < MAX_TERRAIN_PAINT_LAYERS; i++) {
180
+ const slot = next.slots[i];
181
+ colors[i].copy(slot?.color ?? colors[i]);
182
+ classes[i] = slot?.classIndex ?? 0;
183
+ roughness[i] = slot?.roughness ?? 0.9;
184
+ scales[i] = slot?.scale ?? 1;
185
+ }
186
+ };
187
+ update(weights);
188
+ return {
189
+ uniforms,
190
+ get classMask() {
191
+ return classMask;
192
+ },
193
+ update,
194
+ dispose: () => {
195
+ uniforms.uPaintA.value.dispose();
196
+ uniforms.uPaintB.value.dispose();
197
+ },
198
+ };
199
+ }
@@ -1,7 +1,7 @@
1
1
  import { type ReactNode } from "react";
2
2
  import type { SceneObject } from "@jgengine/core/scene/objectStore";
3
3
  import { type StaticObjectBatch } from "./staticObjectBatches.js";
4
- /** Render compatible authored props as shared model draws, preserving unsupported models. @internal */
4
+ /** Render compatible authored or `scenery` props as shared model draws, preserving unsupported models. @internal */
5
5
  export declare function StaticObjectModels({ batch, single }: {
6
6
  batch: StaticObjectBatch;
7
7
  single: (object: SceneObject) => ReactNode;
@@ -69,7 +69,7 @@ function LoadedStaticObjects({ batch, single }) {
69
69
  return _jsx(_Fragment, { children: batch.objects.map(single) });
70
70
  return _jsxs(_Fragment, { children: [harvested.sources.map((source, index) => objects.length === 0 ? null : _jsx(ObjectSource, { source: source, objects: objects, batch: batch }, index)), excluded.map((id) => { const live = ctx.scene.object.get(id); return live === null ? null : single(live); })] });
71
71
  }
72
- /** Render compatible authored props as shared model draws, preserving unsupported models. @internal */
72
+ /** Render compatible authored or `scenery` props as shared model draws, preserving unsupported models. @internal */
73
73
  export function StaticObjectModels({ batch, single }) {
74
74
  return _jsx(IsolatedModelPart, { model: batch.model, seam: "object", children: _jsx(LoadedStaticObjects, { batch: batch, single: single }) });
75
75
  }
@@ -175,7 +175,7 @@ function WorldActors({ entitySprites, entityModels, objectModels, objectStyles,
175
175
  }
176
176
  return [{ object, model, custom, style, authored: authoredIds.has(instanceId), catalog: ctx.scene.object.catalog(instanceId) }];
177
177
  });
178
- const grouped = groupStaticObjects(candidates.map((candidate) => ({ ...candidate, custom: candidate.custom !== undefined && candidate.custom !== null })), 24, batchCache.current);
178
+ const grouped = groupStaticObjects(candidates.map((candidate) => ({ ...candidate, custom: candidate.custom !== undefined && candidate.custom !== null })), undefined, batchCache.current);
179
179
  batchCache.current = new Map(grouped.batches.map((batch) => [batch.key, batch]));
180
180
  const candidatesById = new Map(candidates.map((candidate) => [candidate.object.instanceId, candidate]));
181
181
  const renderSingle = (object) => {
@@ -24,7 +24,11 @@ export declare function canBatchStaticObject(candidate: StaticObjectCandidate):
24
24
  export declare function isCompatibleStaticObject(object: SceneObject, model: ModelConfig | undefined, catalog: GameContextObjectEntry | null): boolean;
25
25
  /** Stable exclusion snapshot; only compatibility transitions allocate. @internal */
26
26
  export declare function incompatibleStaticObjectIds(batch: StaticObjectBatch, get: (id: string) => SceneObject | null, catalog: (id: string) => GameContextObjectEntry | null, previous: readonly string[], fallbackObjects?: Map<string, SceneObject>): readonly string[];
27
- /** Groups compatible authored props above eight placements per spatial cell. @internal */
27
+ /** Batch cell edge in metres: one instanced draw per model per cell, culled as a unit. @internal */
28
+ export declare const STATIC_BATCH_CHUNK = 64;
29
+ /** Smallest same-model group worth an instanced draw; below this each prop draws on its own. @internal */
30
+ export declare const STATIC_BATCH_MIN = 3;
31
+ /** Groups compatible authored or `scenery` props, at least {@link STATIC_BATCH_MIN} per spatial cell. @internal */
28
32
  export declare function groupStaticObjects(candidates: readonly StaticObjectCandidate[], chunkSize?: number, previous?: ReadonlyMap<string, StaticObjectBatch>): {
29
33
  batches: StaticObjectBatch[];
30
34
  singles: StaticObjectCandidate[];
@@ -3,7 +3,7 @@ import { POINTER_OBJECT_INSTANCES_KEY } from "../pointer/pointerService.js";
3
3
  /** The same compatibility gate is used when live placements change after grouping. @internal */
4
4
  export function canBatchStaticObject(candidate) {
5
5
  const { object, model, catalog } = candidate;
6
- return candidate.authored && !candidate.custom && candidate.style === undefined &&
6
+ return (candidate.authored || catalog?.scenery === true) && !candidate.custom && candidate.style === undefined &&
7
7
  isCompatibleStaticObject(object, model, catalog);
8
8
  }
9
9
  /** Checks a live placement without constructing a grouping candidate. @internal */
@@ -15,7 +15,7 @@ export function isCompatibleStaticObject(object, model, catalog) {
15
15
  (catalog?.breakable === undefined || catalog.breakable === false) && catalog?.slotInventory === undefined &&
16
16
  (animation === undefined || animation === "auto" || animation === "none") && model.ik === undefined &&
17
17
  (model.scale ?? 1) >= 0 && (model.parts?.length ?? 0) === 0 && (model.attachments?.length ?? 0) === 0 &&
18
- model.partMotion === undefined && model.material?.maps === undefined && (model.materialAssignments?.length ?? 0) === 0 &&
18
+ model.partMotion === undefined && model.surfaceDetail !== false && model.material?.maps === undefined && (model.materialAssignments?.length ?? 0) === 0 &&
19
19
  (model.hiddenNodes === undefined || (Array.isArray(model.hiddenNodes) && model.hiddenNodes.length === 0));
20
20
  }
21
21
  /** Stable exclusion snapshot; only compatibility transitions allocate. @internal */
@@ -49,8 +49,12 @@ export function incompatibleStaticObjectIds(batch, get, catalog, previous, fallb
49
49
  return live !== null && !compatible(live) ? [live.instanceId] : [];
50
50
  });
51
51
  }
52
- /** Groups compatible authored props above eight placements per spatial cell. @internal */
53
- export function groupStaticObjects(candidates, chunkSize = 24, previous) {
52
+ /** Batch cell edge in metres: one instanced draw per model per cell, culled as a unit. @internal */
53
+ export const STATIC_BATCH_CHUNK = 64;
54
+ /** Smallest same-model group worth an instanced draw; below this each prop draws on its own. @internal */
55
+ export const STATIC_BATCH_MIN = 3;
56
+ /** Groups compatible authored or `scenery` props, at least {@link STATIC_BATCH_MIN} per spatial cell. @internal */
57
+ export function groupStaticObjects(candidates, chunkSize = STATIC_BATCH_CHUNK, previous) {
54
58
  const groups = new Map();
55
59
  const singles = [];
56
60
  for (const candidate of candidates) {
@@ -70,7 +74,7 @@ export function groupStaticObjects(candidates, chunkSize = 24, previous) {
70
74
  }
71
75
  const batches = [];
72
76
  for (const { batch, candidates: members } of groups.values()) {
73
- if (batch.objects.length > 8) {
77
+ if (batch.objects.length >= STATIC_BATCH_MIN) {
74
78
  const cached = previous?.get(batch.key);
75
79
  batches.push(cached !== undefined && cached.objects.length === batch.objects.length &&
76
80
  cached.objects.every((object, index) => object.instanceId === batch.objects[index].instanceId) ? cached : batch);
@@ -6,6 +6,8 @@ export interface WorldBarSample {
6
6
  x: number;
7
7
  y: number;
8
8
  percent: number;
9
+ /** Screen pixels per world metre at the anchor, so painted bars shrink with distance and zoom. */
10
+ pxPerMeter?: number;
9
11
  }
10
12
  export interface Projectable {
11
13
  set(x: number, y: number, z: number): this;
@@ -63,9 +65,19 @@ export declare function collectNameplateSamples(ctx: GameContext, statId: string
63
65
  width: number;
64
66
  height: number;
65
67
  }, into: NameplateSample[], project: Projectable, maxDistance?: number, occlude?: boolean, options?: WorldOverlaySampleOptions): number;
68
+ /**
69
+ * Painted bar size for a sample: about {@link WORLD_BAR_METERS} wide at the anchor's on-screen
70
+ * scale, clamped between a still-readable minimum and the configured maximum, with height in
71
+ * proportion. Samples without a scale (caller-painted screen samples) keep the maximum.
72
+ * @internal
73
+ */
74
+ export declare function worldBarSize(pxPerMeter: number | undefined, maxWidthPx?: number, maxHeightPx?: number): {
75
+ width: number;
76
+ height: number;
77
+ };
66
78
  /** @internal */
67
79
  export declare function paintWorldBarSamples(canvas: {
68
80
  width: number;
69
81
  height: number;
70
82
  getContext(kind: "2d"): CanvasRenderingContext2D | null;
71
- }, samples: readonly WorldBarSample[], dpr: number, barWidthPx?: number, barHeightPx?: number): void;
83
+ }, samples: readonly WorldBarSample[], dpr: number, maxBarWidthPx?: number, maxBarHeightPx?: number): void;