-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathelevationRenderRequest.ts
More file actions
489 lines (482 loc) · 20.9 KB
/
Copy pathelevationRenderRequest.ts
File metadata and controls
489 lines (482 loc) · 20.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
import type { Point } from "../distanceFromNearestPoint";
import { CLIFF_MARK_BACK_PX, CLIFF_MARK_SIZE_PX } from "../cliffs/cliffCatalog";
import type { CliffControls, CliffSettingsInput } from "../cliffs/cliffCatalog";
import type { VulcanusResourceControls } from "../eval/ctx";
import type { EnemyControls } from "../enemies/enemyCatalog";
import type { Planet } from "../../model/planets";
import { PLACEMENT_MARK_RADIUS_PX } from "../placement/placementRoll";
import type { ResourceControlLevers } from "../resources/resolveResource";
import type { RockControls } from "../rocks/rockCatalog";
import { renderCliffs } from "./renderCliffs";
import { renderElevation } from "./renderElevation";
import { renderEnemies } from "./renderEnemies";
import { renderResources } from "./renderResources";
import { renderRocks } from "./renderRocks";
import { renderTerrain } from "./renderTerrain";
import { renderTrees } from "./renderTrees";
import { renderVulcanusCliffs } from "./renderVulcanusCliffs";
import { makeVulcanusStack } from "../tiles/vulcanusCatalog";
import { renderVulcanusResources } from "./renderVulcanusResources";
import { renderVulcanusRocks } from "./renderVulcanusRocks";
import { renderVulcanusTerrain } from "./renderVulcanusTerrain";
/** A render job posted to the worker. `id` tags the response for staleness. */
export interface ElevationRenderRequest {
id: number;
seed0: number;
width: number;
height: number;
originX: number;
originY: number;
tilesPerPixel: number;
waterLevel: number;
segmentationMultiplier: number;
startingPositions: Point[];
/**
* Which planet's terrain to render (Task 11). Default `"nauvis"` when
* omitted - existing (Nauvis) requests are byte-unchanged. Only the
* terrain-family views (`"terrain"`/`"resources"`/`"enemies"`/`"cliffs"`/
* `"trees"`/`"rocks"`/`"all"`) consult this; `"elevation"` is unaffected
* (it dispatches purely on `mapType`, which only spans the Nauvis-family
* elevation trees).
*
* `"vulcanus"` renders through `renderVulcanusTerrain` (Task 10's tile
* resolver) instead of `renderTerrain`. Of the five overlays
* (resources/enemies/cliffs/trees/rocks), only `resources` has a Vulcanus
* port (V2, `renderVulcanusResources`) - the other four have no Vulcanus
* meaning, so a terrain-family view that asks for one still gets plain
* Vulcanus terrain colors rather than a Nauvis field composited on top.
*/
planet?: Planet;
/** Omitted => the game's real lake positions are computed inside the render. */
startingLakePositions?: Point[];
/**
* Climate controls (Task 12b) - consumed only when `view: "terrain"`; the
* elevation renderers ignore these. Each defaults to the game's default
* (freq 1, bias 0, starting-area size/frequency 1) when omitted.
*/
moistureFrequency?: number;
moistureBias?: number;
/** Only `trees` consumes temperature; tile selection is aux + moisture. */
temperatureFrequency?: number;
temperatureBias?: number;
auxFrequency?: number;
auxBias?: number;
startingAreaMoistureSize?: number;
startingAreaMoistureFrequency?: number;
/** Which elevation tree to render. Default "lakes". */
mapType?: "lakes" | "nauvis" | "island";
/**
* Which render to run: the water/land elevation mask ("elevation"), the full
* terrain-tile color render ("terrain"), the terrain with the resource-patch
* overlay composited on top ("resources"), the terrain with the enemy-base
* footprint overlay composited on top ("enemies"), the terrain with the
* cliff footprint overlay composited on top ("cliffs"), the terrain with the
* tree-density blend composited on top ("trees"), the terrain with the rock
* footprint overlay composited on top ("rocks"), or the terrain with all
* five overlays composited on top at once ("all"). Default "elevation".
* renderTerrain (and therefore "resources"/"enemies"/"cliffs"/"trees"/"rocks"/"all") always uses the
* Nauvis climate + tile catalog (see renderTerrain.ts), so it is only faithful
* when `mapType` is "nauvis" - callers (the preview panel) disable those
* toggles for lakes/island presets rather than send an unfaithful request here.
*/
view?: "elevation" | "terrain" | "resources" | "enemies" | "cliffs" | "trees" | "rocks" | "all";
/**
* Per-resource control levers (control:<res>:frequency|size|richness), keyed by
* controlName - consumed only when `view: "resources"`. Missing entries default
* to 1/1/1 inside the resolver.
*/
resourceControls?: Record<string, ResourceControlLevers>;
/**
* Vulcanus resource control levers - consumed only when `planet: "vulcanus"`
* and `view: "resources"`. Defaults to all-neutral.
*/
vulcanusResourceControls?: VulcanusResourceControls;
/**
* Escape hatch for `test/vulcanusStackCache.spec.ts`, which has to render the
* same request BOTH ways to prove the shared cached stack is byte-identical
* to per-renderer stacks. Defaults to the shared stack; there is no reason to
* set this outside that test.
*/
unsharedStacks?: boolean;
/**
* The enemy-base autoplace control's frequency/size (control:enemy-base:*) -
* consumed only when `view: "enemies"`. Defaults to `{ frequency: 1, size: 1 }`
* when omitted.
*/
enemyControls?: EnemyControls;
/**
* The `nauvis_cliff` autoplace control's frequency/size (control:nauvis_cliff:*,
* size doubles as continuity) - consumed only when `view: "cliffs"`. Defaults to
* `{ frequency: 1, continuity: 1 }` when omitted.
*/
cliffControls?: CliffControls;
/**
* The cliff-related MapGenSettings fields (cliff_elevation_0,
* cliff_elevation_interval, cliff richness) - consumed only when
* `view: "cliffs"`. Defaults to `{ cliffElevation0: 10, cliffElevationInterval:
* 40, richness: 1 }` (the game's defaults) when omitted.
*/
cliffSettings?: CliffSettingsInput;
/**
* The `trees` autoplace control's frequency/size (control:trees:*) - consumed
* only when `view: "trees"` or `"all"`. Defaults to `{ frequency: 1, size: 1 }`.
*/
treeControls?: { readonly frequency: number; readonly size: number };
/**
* The `rocks` autoplace control's frequency/size (control:rocks:*) - consumed
* only when `view: "rocks"` or `"all"`. Defaults to `{ frequency: 1, size: 1 }`.
*/
rockControls?: RockControls;
/**
* The full image this request is one tile of, when the renderer is tiling.
* Absent means the request *is* the whole image (the single-render path).
*
* Used for exactly one thing: clamping the widened cliff cell-query box, so a
* tiled render reproduces the untiled render byte for byte - including at the
* image border, where the untiled render drops cliff cells centered outside
* the image. `tilesPerPixel` is shared with the request.
*/
fullImage?: {
readonly originX: number;
readonly originY: number;
readonly width: number;
readonly height: number;
};
}
/** The rendered pixels, with `buffer` posted back as a transferable. */
export interface ElevationRenderResult {
id: number;
buffer: ArrayBuffer;
width: number;
height: number;
}
/** A world-tile box, inclusive lower / exclusive upper on both axes. */
export interface WorldBox {
x0: number;
y0: number;
x1: number;
y1: number;
}
/**
* The world box to sweep/enumerate for `req`: its own pixel box, widened by
* `radiusPx` pixels' worth of world tiles so a mark centered just outside this
* tile still owes it pixels, then intersected with the full image so the outer
* border keeps the untiled behavior.
*
* The halo is exact rather than conservative, and it is **asymmetric whenever
* the mark is**. A mark at world `wx` maps to pixel `px = floor((wx - originX) /
* tpp)` and paints `px - back .. px + fwd`, so it touches this tile
*
* on the low side iff `px + fwd >= 0` -> widen x0 by `fwd * tpp`
* on the high side iff `px - back <= w - 1` -> widen x1 by `back * tpp`
*
* (using `floor(v) >= -k` iff `v >= -k` for integer `k`, and pairing with an
* inclusive-lower / exclusive-upper enumeration). Note the directions CROSS: a
* mark that reaches far *backwards* has to be caught from *ahead* of the tile.
*
* A symmetric `max(back, fwd)` on both sides is correct but not free. The cliff
* pass quantizes its enumeration to 32-tile chunks, so one surplus tile of halo
* can pull in a whole extra chunk per axis - measured at 512x512 tiled 16 ways,
* a symmetric 2/2 halo cost 24,336 cliffiness evaluations against 17,424 for the
* exact 1/2 one, a **1.40x** overhead for zero pixels of difference. See
* `docs/noise/vulcanus-cliffs-NOTES.md`.
*/
function haloQueryBox(req: ElevationRenderRequest, backPx: number, fwdPx: number): WorldBox {
const tpp = req.tilesPerPixel;
const x0 = req.originX;
const y0 = req.originY;
const x1 = req.originX + req.width * tpp;
const y1 = req.originY + req.height * tpp;
const full = req.fullImage;
if (!full) return { x0, y0, x1, y1 };
// Crossed on purpose - see the doc above.
const lo = fwdPx * tpp;
const hi = backPx * tpp;
return {
x0: Math.max(x0 - lo, full.originX),
y0: Math.max(y0 - lo, full.originY),
x1: Math.min(x1 + hi, full.originX + full.width * tpp),
y1: Math.min(y1 + hi, full.originY + full.height * tpp),
};
}
/**
* The world box to enumerate cliff cells over for `req`, so a cell whose block
* reaches into this tile is always enumerated.
*
* The cliff block is **not** symmetric: it spans `px - CLIFF_MARK_BACK_PX ..
* px + CLIFF_MARK_SIZE_PX - CLIFF_MARK_BACK_PX - 1`, i.e. 2 back and 1 forward,
* which is what anchors it on the cell's own 4-tile footprint. This used to
* widen by `CLIFF_MARK_BACK_PX` in both directions ("the larger of the block's
* two directions"), a description that fit the 5x5 centred mark it was written
* for and outlived it. Correct, and one tile too wide on the low side.
*
* Exported for direct unit testing: the tiled-equals-untiled gate pins the
* widening (drop it and the gate fails) but cannot pin the clamp, which only
* changes pixels when a cliff cell happens to sit just outside the image border
* next to non-water terrain.
*/
export function cliffCellQueryBox(req: ElevationRenderRequest): WorldBox {
return haloQueryBox(req, CLIFF_MARK_BACK_PX, CLIFF_MARK_SIZE_PX - CLIFF_MARK_BACK_PX - 1);
}
/**
* The world box to sweep for a placement roll that paints a 3x3 mark -
* `haloQueryBox` at `PLACEMENT_MARK_RADIUS_PX`. Shared by Nauvis enemy bases
* (`renderEnemies.ts`), Vulcanus geysers (`renderVulcanusResources.ts`) and
* Nauvis crude oil (`renderResources.ts`).
*
* A 3x3 mark straddles worker-tile seams, hence the halo (a spawner is
* 7.4 x 6.4 tiles, a geyser 2.8 x 2.8, and both are rare enough that a dot would
* vanish). Each renderer documents its sweep side; `test/tiledEquality.spec.ts`
* is what fails without it, and it carries a separate case per overlay because a
* window dense in one is empty of the other.
*
* The **Vulcanus rock overlay also uses this** and also paints a 3x3 mark
* (`VULCANUS_ROCK_MARK_RADIUS_PX`). An older version of this comment said "both
* rock overlays paint a 1x1 pixel and need no equivalent", which stopped being
* true when the Vulcanus rock mark grew; only the Nauvis rock overlay is 1x1.
*
* This halo is exact and cannot be tightened - unlike the cliff one it faces
* (`cliffCellQueryBox`), the mark really is symmetric. It is, however, where the
* remaining tiled-vs-whole cost lives: one pixel of widening crosses a 32-tile
* chunk boundary, so an interior tile resolves 6 chunks per axis instead of 4
* and every seam chunk is resolved by both neighbours (1.89x `resolveChunk`
* calls, measured). That is structural rather than a defect - a rock one pixel
* outside genuinely owes this tile pixels, and knowing whether it exists means
* resolving its chunk. See `docs/noise/vulcanus-cliffs-NOTES.md`.
*/
export function placementMarkSweepBox(req: ElevationRenderRequest): WorldBox {
// Genuinely symmetric: a 3x3 mark centred on its pixel.
return haloQueryBox(req, PLACEMENT_MARK_RADIUS_PX, PLACEMENT_MARK_RADIUS_PX);
}
/**
* Pure render step shared by the worker and its tests: run renderElevation or
* renderTerrain (per `req.view`) and hand back the transferable RGBA buffer. No
* Worker or DOM canvas involved.
*/
export function runRenderRequest(req: ElevationRenderRequest): ElevationRenderResult {
const planet = req.planet ?? "nauvis";
let image: ImageData;
if (
req.view === "terrain" ||
req.view === "resources" ||
req.view === "enemies" ||
req.view === "cliffs" ||
req.view === "trees" ||
req.view === "rocks" ||
req.view === "all"
) {
if (planet === "vulcanus") {
// Vulcanus has its own resource and cliff overlays. The remaining three
// Nauvis overlays (enemies, trees, rocks) have no Vulcanus port, so a
// terrain-family view that asks for one still gets plain terrain rather
// than a Nauvis field composited onto Vulcanus colors.
const wantsResources = req.view === "resources" || req.view === "all";
// ONE stack for the whole composite. Two things make this pay, and both
// are needed: the overlays reuse the field objects terrain built, and
// those objects carry a cross-traversal cache (`memoRegion`), so a pass
// that walks the image in a different order from terrain - the rock
// overlay resolves whole 32x32 chunks - still hits values terrain
// computed. Sharing without the cache buys almost nothing, because
// `memoXY` holds only the last coordinate.
const stack =
req.unsharedStacks === true
? undefined
: makeVulcanusStack(
{
seed0: req.seed0,
startingPositions: req.startingPositions,
vulcanusResourceControls: req.vulcanusResourceControls,
},
{ cacheShared: true },
);
image = renderVulcanusTerrain({
seed0: req.seed0,
width: req.width,
height: req.height,
originX: req.originX,
originY: req.originY,
tilesPerPixel: req.tilesPerPixel,
ctx: {
startingPositions: req.startingPositions,
vulcanusResourceControls: req.vulcanusResourceControls,
},
stack,
});
// Resources paint first, then the two obstruction overlays on top, so a
// cliff or a rock crossing an ore patch still reads as the thing that is
// in the way. Cliffs last matches the Nauvis order below, where
// renderCliffs is the final pass.
if (wantsResources) {
renderVulcanusResources(image, {
seed0: req.seed0,
originX: req.originX,
originY: req.originY,
tilesPerPixel: req.tilesPerPixel,
ctx: {
startingPositions: req.startingPositions,
vulcanusResourceControls: req.vulcanusResourceControls,
},
// The geyser rolls and paints a 3x3 mark; the three solid ores
// threshold and paint 1x1, and ignore this.
sweepBox: placementMarkSweepBox(req),
stack,
});
}
if (req.view === "rocks" || req.view === "all") {
renderVulcanusRocks(image, {
seed0: req.seed0,
originX: req.originX,
originY: req.originY,
tilesPerPixel: req.tilesPerPixel,
ctx: { startingPositions: req.startingPositions },
sweepBox: placementMarkSweepBox(req),
sharedStack: stack,
});
}
if (req.view === "cliffs" || req.view === "all") {
renderVulcanusCliffs(image, {
seed0: req.seed0,
originX: req.originX,
originY: req.originY,
tilesPerPixel: req.tilesPerPixel,
ctx: { startingPositions: req.startingPositions },
cellQueryBox: cliffCellQueryBox(req),
sharedStack: stack,
});
}
return { id: req.id, buffer: image.data.buffer, width: req.width, height: req.height };
}
image = renderTerrain({
seed0: req.seed0,
width: req.width,
height: req.height,
originX: req.originX,
originY: req.originY,
tilesPerPixel: req.tilesPerPixel,
ctx: {
segmentationMultiplier: req.segmentationMultiplier,
startingPositions: req.startingPositions,
moistureFrequency: req.moistureFrequency,
moistureBias: req.moistureBias,
auxFrequency: req.auxFrequency,
auxBias: req.auxBias,
startingAreaMoistureSize: req.startingAreaMoistureSize,
startingAreaMoistureFrequency: req.startingAreaMoistureFrequency,
},
});
if (req.view === "trees" || req.view === "all") {
renderTrees(image, {
seed0: req.seed0,
originX: req.originX,
originY: req.originY,
tilesPerPixel: req.tilesPerPixel,
treesFrequency: req.treeControls?.frequency ?? 1,
treesSize: req.treeControls?.size ?? 1,
segmentationMultiplier: req.segmentationMultiplier,
moistureFrequency: req.moistureFrequency,
moistureBias: req.moistureBias,
temperatureFrequency: req.temperatureFrequency,
temperatureBias: req.temperatureBias,
startingAreaMoistureSize: req.startingAreaMoistureSize,
startingAreaMoistureFrequency: req.startingAreaMoistureFrequency,
startingPositions: req.startingPositions,
});
}
if (req.view === "resources" || req.view === "all") {
renderResources(image, {
seed0: req.seed0,
originX: req.originX,
originY: req.originY,
tilesPerPixel: req.tilesPerPixel,
controls: req.resourceControls ?? {},
startingPositions: req.startingPositions,
segmentationMultiplier: req.segmentationMultiplier,
waterLevel: req.waterLevel,
startingLakePositions: req.startingLakePositions,
moistureFrequency: req.moistureFrequency,
moistureBias: req.moistureBias,
auxFrequency: req.auxFrequency,
auxBias: req.auxBias,
startingAreaMoistureSize: req.startingAreaMoistureSize,
startingAreaMoistureFrequency: req.startingAreaMoistureFrequency,
sweepBox: placementMarkSweepBox(req),
});
}
// Rocks paint after resources (and cliffs last of all) so an obstruction
// crossing an ore patch reads as the obstruction - same order as the
// Vulcanus branch above. Trees stay under resources: a forest is cleared,
// not an obstacle you route around.
if (req.view === "rocks" || req.view === "all") {
renderRocks(image, {
seed0: req.seed0,
originX: req.originX,
originY: req.originY,
tilesPerPixel: req.tilesPerPixel,
controls: req.rockControls ?? { frequency: 1, size: 1 },
segmentationMultiplier: req.segmentationMultiplier,
moistureFrequency: req.moistureFrequency,
moistureBias: req.moistureBias,
auxFrequency: req.auxFrequency,
auxBias: req.auxBias,
startingAreaMoistureSize: req.startingAreaMoistureSize,
startingAreaMoistureFrequency: req.startingAreaMoistureFrequency,
startingPositions: req.startingPositions,
sweepBox: placementMarkSweepBox(req),
});
}
if (req.view === "enemies" || req.view === "all") {
renderEnemies(image, {
seed0: req.seed0,
originX: req.originX,
originY: req.originY,
tilesPerPixel: req.tilesPerPixel,
controls: req.enemyControls ?? { frequency: 1, size: 1 },
startingPositions: req.startingPositions,
segmentationMultiplier: req.segmentationMultiplier,
moistureFrequency: req.moistureFrequency,
moistureBias: req.moistureBias,
auxFrequency: req.auxFrequency,
auxBias: req.auxBias,
startingAreaMoistureSize: req.startingAreaMoistureSize,
startingAreaMoistureFrequency: req.startingAreaMoistureFrequency,
sweepBox: placementMarkSweepBox(req),
});
}
if (req.view === "cliffs" || req.view === "all") {
renderCliffs(image, {
seed0: req.seed0,
originX: req.originX,
originY: req.originY,
tilesPerPixel: req.tilesPerPixel,
controls: req.cliffControls ?? { frequency: 1, continuity: 1 },
settings: req.cliffSettings ?? {
cliffElevation0: 10,
cliffElevationInterval: 40,
richness: 1,
},
segmentationMultiplier: req.segmentationMultiplier,
waterLevel: req.waterLevel,
startingPositions: req.startingPositions,
startingLakePositions: req.startingLakePositions,
cellQueryBox: cliffCellQueryBox(req),
});
}
} else {
image = renderElevation({
seed0: req.seed0,
width: req.width,
height: req.height,
originX: req.originX,
originY: req.originY,
tilesPerPixel: req.tilesPerPixel,
mapType: req.mapType,
ctx: {
waterLevel: req.waterLevel,
segmentationMultiplier: req.segmentationMultiplier,
startingPositions: req.startingPositions,
startingLakePositions: req.startingLakePositions,
},
});
}
return { id: req.id, buffer: image.data.buffer, width: req.width, height: req.height };
}