fuzzyworld

tower / files / src / terrain / palette.ts

palette.ts

4.9 KB · history · blame

1/**
2 * What the ground looks like, from what it is. See world-generation.md §6.2.
3 *
4 * Tiles carry material weights, not colours (tile.ts). This turns weights and an
5 * elevation into colour, twice over from one table: in TypeScript for the map, and as
6 * GLSL for the 3D views — so the map and the view are the same colours by
7 * construction rather than by two lists kept in step by hand.
8 *
9 * The ramps are v1's hypsometric tints, sRGB 0..255 against elevation in metres.
10 */
11type Ramp = readonly (readonly [number, readonly [number, number, number]])[]
12
13export const RAMPS: Record<'plain' | 'desert' | 'forest' | 'rock' | 'sandstone', Ramp> = {
14 plain: [[-40, [104, 118, 88]], [120, [118, 130, 92]], [340, [132, 138, 98]],
15 [700, [140, 138, 106]], [1600, [146, 138, 120]]],
16 desert: [[400, [198, 178, 132]], [700, [186, 160, 112]], [1000, [166, 138, 96]],
17 [1400, [148, 120, 86]]],
18 forest: [[80, [52, 86, 54]], [260, [62, 100, 58]], [500, [76, 108, 64]], [1000, [98, 114, 76]]],
19 rock: [[600, [104, 106, 96]], [1400, [124, 122, 114]], [2200, [146, 142, 136]],
20 [2900, [166, 162, 156]]],
21 /** Desert rock: the sandstones and mudstones a mesa is cut from, warm and reddened. */
22 sandstone: [[400, [190, 132, 94]], [800, [176, 114, 78]], [1200, [160, 100, 70]],
23 [1800, [146, 96, 72]]],
24}
25export const SNOW = [247, 249, 253] as const
26/** Salt: white, but a dirtier and warmer white than snow (world-generation.md §4.5). */
27export const SALT = [232, 227, 213] as const
28/**
29 * Sea ice with snow on it (D-090): white, but a bluer, colder white than snow on land, and
30 * greyer — it is lit by dusk, and wind-packed rather than fresh.
31 */
32export const ICE = [214, 224, 232] as const
33/** Shallow water, and how much darker it gets at full depth. */
34export const WATER = [58, 96, 132] as const
35export const WATER_DEEPEN = [20, 26, 24] as const
36
37export function ramp(r: Ramp, h: number, out: number[]): void {
38 let c = r[0]![1]
39 if (h > r[0]![0]) {
40 c = r[r.length - 1]![1]
41 for (let i = 1; i < r.length; i++) {
42 if (h <= r[i]![0]) {
43 const t = (h - r[i - 1]![0]) / (r[i]![0] - r[i - 1]![0])
44 const a = r[i - 1]![1], b = r[i]![1]
45 out[0] = a[0] + (b[0] - a[0]) * t
46 out[1] = a[1] + (b[1] - a[1]) * t
47 out[2] = a[2] + (b[2] - a[2]) * t
48 return
49 }
50 }
51 }
52 out[0] = c[0]; out[1] = c[1]; out[2] = c[2]
53}
54
55/**
56 * sRGB 0..255 for a material mix at an elevation. `m` is tile.ts MATERIALS order:
57 * plain, desert, forest, rock, snow, water, depth, bed, salt, sandstone.
58 */
59export function colourOf(m: ArrayLike<number>, h: number, out: number[]): void {
60 const tmp = [0, 0, 0]
61 let r = 0, g = 0, b = 0, sum = 0
62 const names = ['plain', 'desert', 'forest', 'rock'] as const
63 for (let i = 0; i < 4; i++) {
64 const w = m[i]!
65 if (w <= 0) continue
66 ramp(RAMPS[names[i]!], h, tmp)
67 r += tmp[0]! * w; g += tmp[1]! * w; b += tmp[2]! * w
68 sum += w
69 }
70 const sandstone = m[9] ?? 0
71 if (sandstone > 0) {
72 ramp(RAMPS.sandstone, h, tmp)
73 r += tmp[0]! * sandstone; g += tmp[1]! * sandstone; b += tmp[2]! * sandstone
74 sum += sandstone
75 }
76 if (sum > 0) { r /= sum; g /= sum; b /= sum }
77 const salt = m[8] ?? 0
78 r += (SALT[0] - r) * salt; g += (SALT[1] - g) * salt; b += (SALT[2] - b) * salt
79 const snow = m[4]!
80 r += (SNOW[0] - r) * snow; g += (SNOW[1] - g) * snow; b += (SNOW[2] - b) * snow
81 const water = m[5]!, d = m[6]!
82 r += (WATER[0] - WATER_DEEPEN[0] * d - r) * water
83 g += (WATER[1] - WATER_DEEPEN[1] * d - g) * water
84 b += (WATER[2] - WATER_DEEPEN[2] * d - b) * water
85 out[0] = r; out[1] = g; out[2] = b
86}
87
88const f = (x: number): string => x.toFixed(4)
89const vec = (c: readonly number[]): string => `vec3(${f(c[0]! / 255)}, ${f(c[1]! / 255)}, ${f(c[2]! / 255)})`
90
91/** A ramp as a GLSL function of elevation in metres, returning sRGB 0..1. */
92function glslRamp(name: string, r: Ramp): string {
93 const lines = [`vec3 ${name}(float h) {`, ` vec3 c = ${vec(r[0]![1])};`]
94 for (let i = 1; i < r.length; i++) {
95 const [h0] = r[i - 1]!, [h1, c1] = r[i]!
96 lines.push(` c = mix(c, ${vec(c1)}, clamp((h - ${f(h0)}) / ${f(h1 - h0)}, 0.0, 1.0));`)
97 }
98 lines.push(' return c;', '}')
99 return lines.join('\n')
100}
101
102/** The palette as GLSL: `groundColour(matA, matB, extra, h)`, `extra` = (salt, sandstone). */
103export const PALETTE_GLSL = [
104 glslRamp('rampPlain', RAMPS.plain),
105 glslRamp('rampDesert', RAMPS.desert),
106 glslRamp('rampForest', RAMPS.forest),
107 glslRamp('rampRock', RAMPS.rock),
108 glslRamp('rampSandstone', RAMPS.sandstone),
109 `vec3 groundColour(vec4 a, vec4 b, vec2 extra, float h) {
110 float sum = max(a.x + a.y + a.z + a.w + extra.y, 1e-3);
111 vec3 c = (rampPlain(h) * a.x + rampDesert(h) * a.y + rampForest(h) * a.z + rampRock(h) * a.w
112 + rampSandstone(h) * extra.y) / sum;
113 c = mix(c, ${vec(SALT)}, extra.x);
114 c = mix(c, ${vec(SNOW)}, b.x);
115 vec3 water = ${vec(WATER)} - ${vec(WATER_DEEPEN)} * b.z;
116 return mix(c, water, b.y);
117}`,
118].join('\n\n')