tower / files / src / terrain / farms.ts
farms.ts
abc0aad0 2026-10-01 Pavel Safronov | 1 | /** |
| 2 | * Farmland: fields of crops, each lit on its own day. See design.md §1.9.7, D-095. | |
| 3 | * | |
| 4 | * Tower can put any light on any field at any hour (§1.3.1), so a farm in Tower does not | |
| 5 | * live by the sun. Each crop gets the day its plant wants: a long-day crop is lit on into | |
| 6 | * the night, a short-day one is darkened early, and every one of them gets darkness, because | |
| 7 | * most flowering plants will not flower without it and some, like tomatoes, are injured by | |
| 8 | * light that never stops. In the day it is lit, a field is lit as at noon from first light | |
| 9 | * to last. What Tower cannot do is make a field darker than the rod's own glow by day | |
| 10 | * (§1.3.2), so a short day ends in dusk rather than in night. | |
| 11 | * | |
| 12 | * The fields are laid out here, as a pure function of where you are, and the same function | |
| 13 | * is written out as GLSL (`FARM_GLSL`), so the map and the 3D view put the same crop in the | |
| 14 | * same field. Hashes are a permutation mod 289, exact in float on both sides. | |
| 15 | */ | |
| 16 | import { CIRCUMFERENCE_M } from '../canon/constants.js' | |
| 17 | ||
| 18 | export interface Crop { | |
| 19 | name: string | |
| 20 | /** sRGB of the flowers, fruit or ripe heads, of the leaves, and how much of the canopy shows the first. */ | |
| 21 | bloom: readonly [number, number, number] | |
| 22 | leaf: readonly [number, number, number] | |
| 23 | cover: number | |
| 24 | /** Planting rows, metres apart, or 0 for a crop sown broadcast. */ | |
| 25 | rowM: number | |
| 26 | /** Hours of light a day Tower gives it. */ | |
| 27 | dayH: number | |
| 28 | /** How often it is planted, relative to the others. */ | |
| 29 | weight: number | |
| 30 | } | |
| 31 | ||
| 32 | /** | |
| 33 | * The crops, in bloom or at their peak. The day lengths are the plants' own: rapeseed, flax, | |
| 34 | * wheat, lavender, poppies and phacelia are long-day plants and flower sooner and fuller under | |
| 35 | * long days; buckwheat and potatoes are short-day (potatoes set tubers on short days); tulips, | |
| 36 | * sunflowers, maize and tomatoes are day-neutral, and tomatoes need about six hours of dark. | |
| 37 | */ | |
| 38 | export const CROPS: readonly Crop[] = [ | |
| 39 | { name: 'red tulips', bloom: [214, 32, 44], leaf: [84, 122, 64], cover: 0.75, rowM: 1.2, dayH: 14, weight: 1.4 }, | |
| 40 | { name: 'yellow tulips', bloom: [246, 204, 38], leaf: [84, 122, 64], cover: 0.75, rowM: 1.2, dayH: 14, weight: 1.2 }, | |
| 41 | { name: 'pink tulips', bloom: [238, 112, 164], leaf: [84, 122, 64], cover: 0.75, rowM: 1.2, dayH: 14, weight: 1.0 }, | |
| 42 | { name: 'white tulips', bloom: [242, 238, 224], leaf: [84, 122, 64], cover: 0.7, rowM: 1.2, dayH: 14, weight: 0.6 }, | |
| 43 | { name: 'lavender', bloom: [134, 104, 186], leaf: [104, 124, 104], cover: 0.7, rowM: 1.6, dayH: 16, weight: 1.2 }, | |
| 44 | { name: 'sunflowers', bloom: [236, 182, 28], leaf: [70, 108, 40], cover: 0.55, rowM: 0.75, dayH: 14, weight: 1.0 }, | |
| 45 | { name: 'rapeseed', bloom: [242, 222, 40], leaf: [118, 148, 52], cover: 0.9, rowM: 0, dayH: 18, weight: 1.1 }, | |
| 46 | { name: 'poppies', bloom: [212, 38, 34], leaf: [108, 140, 62], cover: 0.5, rowM: 0, dayH: 16, weight: 0.8 }, | |
| 47 | { name: 'flax', bloom: [104, 146, 222], leaf: [108, 140, 82], cover: 0.6, rowM: 0, dayH: 18, weight: 0.7 }, | |
| 48 | { name: 'phacelia', bloom: [148, 136, 214], leaf: [88, 120, 70], cover: 0.7, rowM: 0, dayH: 16, weight: 0.6 }, | |
| 49 | { name: 'buckwheat', bloom: [240, 232, 226], leaf: [96, 134, 66], cover: 0.6, rowM: 0, dayH: 12, weight: 0.5 }, | |
| 50 | { name: 'maize', bloom: [192, 172, 92], leaf: [86, 128, 48], cover: 0.15, rowM: 0.75, dayH: 14, weight: 0.8 }, | |
| 51 | { name: 'potatoes', bloom: [232, 230, 240], leaf: [60, 108, 50], cover: 0.25, rowM: 0.9, dayH: 12, weight: 0.7 }, | |
| 52 | { name: 'tomatoes', bloom: [200, 46, 38], leaf: [52, 100, 46], cover: 0.2, rowM: 1.5, dayH: 16, weight: 0.5 }, | |
| 53 | { name: 'wheat', bloom: [216, 186, 104], leaf: [150, 150, 70], cover: 0.95, rowM: 0, dayH: 18, weight: 0.8 }, | |
| 54 | { name: 'lettuce', bloom: [136, 192, 80], leaf: [110, 170, 70], cover: 0.5, rowM: 0.4, dayH: 14, weight: 0.4 }, | |
| 55 | ] | |
| 56 | ||
| 57 | /** Bare soil between the rows, and the farm tracks. */ | |
| 58 | export const SOIL = [112, 88, 64] as const | |
| 59 | export const TRACK = [150, 126, 96] as const | |
| 60 | ||
| 61 | /** | |
| 62 | * The fields are laid out the way farmland grows, not on a grid (D-097). The land is first | |
| 63 | * divided into **blocks** about 1.5 km across — irregular, a jittered Voronoi of the surface — | |
| 64 | * with farm tracks along their edges. Each block was taken in hand on its own terms: its fields | |
| 65 | * run at its own angle, are its own size (220 to 440 m), and are drawn out long by its own | |
| 66 | * stretch, as fields cleared at different times by different hands are. Inside a block the | |
| 67 | * fields are the cells of a second Voronoi in the block's own frame — straight edges at odd | |
| 68 | * angles, no two alike — and a third of them are cut again into two to four long strips, as | |
| 69 | * smallholdings are. So no line runs further than a block, and no two blocks agree. | |
| 70 | * | |
| 71 | * The block lattice closes round the ring on a whole number of blocks, and everything inside | |
| 72 | * a block is measured from the block's own centre, so the pattern has no seam and its numbers | |
| 73 | * stay small enough for float32. | |
| 74 | */ | |
| 75 | export const BLOCK_M = 1500 | |
| 76 | export const BLOCKS_AROUND = Math.round(CIRCUMFERENCE_M / BLOCK_M) | |
| 77 | export const BLOCK_A_M = CIRCUMFERENCE_M / BLOCKS_AROUND | |
| 78 | /** Farm tracks along the blocks' edges, and the grass margin between fields. */ | |
| 79 | export const TRACK_M = 6 | |
| 80 | export const MARGIN_M = 1.5 | |
| 81 | ||
| 82 | const permute = (x: number): number => ((34 * x + 1) * x) % 289 | |
| 83 | const mod = (x: number, m: number): number => ((x % m) + m) % m | |
| 84 | /** 0..1 from a cell and a salt, as the GLSL computes it. */ | |
| 85 | export function farmHash(i: number, j: number, salt: number): number { | |
| 86 | return permute(mod(permute(mod(permute(mod(i, 289)) + mod(j, 289), 289)) + salt, 289)) / 289 | |
| 87 | } | |
| 88 | ||
| 89 | /** The crop table's cumulative weights, 0..1, for picking by a hash. */ | |
| 90 | const totalWeight = CROPS.reduce((s, c) => s + c.weight, 0) | |
| 91 | export const CROP_EDGES = CROPS.map((_, i) => CROPS.slice(0, i + 1).reduce((s, c) => s + c.weight, 0) / totalWeight) | |
| 92 | ||
| 93 | export function pickCrop(h: number): number { | |
| 94 | const i = CROP_EDGES.findIndex((e) => h < e) | |
| 95 | return i < 0 ? CROPS.length - 1 : i | |
| 96 | } | |
| 97 | ||
| 98 | export interface Field { | |
| 99 | crop: number | |
| 100 | /** Metres to the nearest edge of the field. */ | |
| 101 | edgeM: number | |
| 102 | /** On a track between blocks, rather than in a field. */ | |
| 103 | track: boolean | |
| 104 | /** Metres across the field's rows, which run along its length. */ | |
| 105 | rowCoordM: number | |
| 106 | /** Where the field is, as the point it was laid out from: z, and a in [0, circumference). */ | |
| 107 | centreZ: number | |
| 108 | centreA: number | |
| 109 | } | |
| 110 | ||
| 111 | interface Nearest { i: number; j: number; x: number; y: number; edge: number } | |
| 112 | ||
| 113 | /** The two nearest jittered cell points in a lattice, and the bisector's distance between them. */ | |
| 114 | function voronoi(x: number, y: number, at: (i: number, j: number) => [number, number]): Nearest { | |
| 115 | const n0 = Math.floor(x), m0 = Math.floor(y) | |
| 116 | let d1 = Infinity, d2 = Infinity | |
| 117 | let b1 = { i: 0, j: 0, x: 0, y: 0 }, b2 = b1 | |
| 118 | for (let dj = -1; dj <= 1; dj++) { | |
| 119 | for (let di = -1; di <= 1; di++) { | |
| 120 | const i = n0 + di, j = m0 + dj | |
| 121 | const [px, py] = at(i, j) | |
| 122 | const d = (px - x) ** 2 + (py - y) ** 2 | |
| 123 | const c = { i, j, x: px, y: py } | |
| 124 | if (d < d1) { d2 = d1; b2 = b1; d1 = d; b1 = c } else if (d < d2) { d2 = d; b2 = c } | |
| 125 | } | |
| 126 | } | |
| 127 | const mx = (b1.x + b2.x) / 2 - x, my = (b1.y + b2.y) / 2 - y | |
| 128 | const len = Math.hypot(b2.x - b1.x, b2.y - b1.y) || 1 | |
| 129 | return { ...b1, edge: (mx * (b2.x - b1.x) + my * (b2.y - b1.y)) / len } | |
| 130 | } | |
| 131 | ||
| 132 | /** The field at a point: `z` along the axis and `a` around, metres. */ | |
| 133 | export function fieldAt(z: number, a: number): Field { | |
| 134 | const a0 = mod(a, CIRCUMFERENCE_M) | |
| 135 | // The block: a Voronoi cell of a jittered lattice, in metres. | |
| 136 | const blk = voronoi(z / BLOCK_M, a0 / BLOCK_A_M, (i, j) => { | |
| 137 | const jw = mod(j, BLOCKS_AROUND) | |
| 138 | return [i + 0.15 + 0.7 * farmHash(i, jw, 11), j + 0.15 + 0.7 * farmHash(i, jw, 23)] | |
| 139 | }) | |
| 140 | const bi = blk.i, bj = mod(blk.j, BLOCKS_AROUND) | |
| 141 | const cz = blk.x * BLOCK_M, ca = blk.y * BLOCK_A_M | |
| 142 | const blockEdgeM = blk.edge * Math.min(BLOCK_M, BLOCK_A_M) | |
| 143 | // The block's own hand: the angle its fields run at, their size and their stretch. | |
| 144 | const ang = farmHash(bi, bj, 7) * Math.PI | |
| 145 | const stretch = 1.3 + 0.9 * farmHash(bi, bj, 8) | |
| 146 | const size = 220 + 220 * farmHash(bi, bj, 9) | |
| 147 | const dz = z - cz, da = a0 - ca | |
| 148 | const cs = Math.cos(ang), sn = Math.sin(ang) | |
| 149 | const ux = cs * dz + sn * da, uy = -sn * dz + cs * da | |
| 150 | // The fields: a Voronoi in the block's frame, drawn out along its length. | |
| 151 | const fld = voronoi(ux / (size * stretch), uy / size, (i, j) => [ | |
| 152 | i + 0.1 + 0.8 * farmHash(bi * 17 + i, bj * 29 + j, 13), | |
| 153 | j + 0.1 + 0.8 * farmHash(bi * 17 + i, bj * 29 + j, 19), | |
| 154 | ]) | |
| 155 | const ki = bi * 17 + fld.i, kj = bj * 29 + fld.j | |
| 156 | // Some fields are cut again into long strips. | |
| 157 | let part = 0, stripEdge = Infinity | |
| 158 | if (farmHash(ki, kj, 31) < 0.35) { | |
| 159 | const k = 2 + Math.floor(farmHash(ki, kj, 37) * 3) | |
| 160 | const t = (uy / size - fld.y) * k + k / 2 | |
| 161 | part = Math.floor(t) | |
| 162 | const f = t - Math.floor(t) | |
| 163 | stripEdge = Math.min(f, 1 - f) * (size / k) | |
| 164 | } | |
| 165 | const fieldEdgeM = fld.edge * size | |
| 166 | // Back to the world, for the point the field was laid out from. | |
| 167 | const px = fld.x * size * stretch, py = fld.y * size | |
| 168 | return { | |
| 169 | crop: pickCrop(farmHash(ki, kj, 41 + part)), | |
| 170 | edgeM: Math.min(blockEdgeM - TRACK_M / 2, fieldEdgeM - MARGIN_M / 2, stripEdge - MARGIN_M / 3), | |
| 171 | track: blockEdgeM < TRACK_M / 2, | |
| 172 | rowCoordM: uy, | |
| 173 | centreZ: cz + cs * px - sn * py, | |
| 174 | centreA: mod(ca + sn * px + cs * py, CIRCUMFERENCE_M), | |
| 175 | } | |
| 176 | } | |
| 177 | ||
| 178 | /** | |
| 179 | * A farm grows (D-097): from its seed outward over the land, one patch of ground at a time, | |
| 180 | * taking the cheapest patch at its edge next — cheap meaning near, and a little lucky, and | |
| 181 | * never water, sand, rock, a range or a slope too steep to plough — until it has the ground | |
| 182 | * it was given. Near-and-lucky is what makes it a blob with lobes and bays rather than a disc | |
| 183 | * or a spider. The patches are a lattice `GROW_M` on a side; a field belongs to the farm if the | |
| 184 | * patch under the point it was laid out from does, so the farm's edge follows its fields'. | |
| 185 | */ | |
| 186 | export const GROW_M = 200 | |
| 187 | export const GROW_AROUND = Math.round(CIRCUMFERENCE_M / GROW_M) | |
| 188 | export const GROW_A_M = CIRCUMFERENCE_M / GROW_AROUND | |
| 189 | ||
| 190 | /** The patch a point is in, as one number. */ | |
| 191 | export function growNode(z: number, a: number): number { | |
| 192 | return Math.round(z / GROW_M) * GROW_AROUND + mod(Math.round(a / GROW_A_M), GROW_AROUND) | |
| 193 | } | |
| 194 | ||
| 195 | export function growFarm( | |
| 196 | seedZ: number, seedA: number, areaM2: number, salt: number, fits: (z: number, a: number) => boolean, | |
| 197 | ): number[] { | |
| 198 | const target = Math.max(1, Math.round(areaM2 / (GROW_M * GROW_A_M))) | |
| 199 | const r0 = Math.sqrt(areaM2 / Math.PI) | |
| 200 | const iz0 = Math.round(seedZ / GROW_M), ia0 = Math.round(seedA / GROW_A_M) | |
| 201 | const key = (iz: number, ia: number): number => { | |
| 202 | const d = Math.hypot((iz - iz0) * GROW_M, (ia - ia0) * GROW_A_M) / r0 | |
| 203 | return 0.5 * d + 0.5 * farmHash(iz + salt, ia * 7 + salt, 61) + 0.25 * farmHash(ia + salt, iz * 3, 67) | |
| 204 | } | |
| 205 | const taken = new Set<number>(), seen = new Set<number>() | |
| 206 | const out: number[] = [] | |
| 207 | // A plain sorted frontier: a few thousand patches at most. | |
| 208 | const frontier: { iz: number; ia: number; k: number }[] = [] | |
| 209 | const id = (iz: number, ia: number): number => iz * GROW_AROUND + mod(ia, GROW_AROUND) | |
| 210 | const offer = (iz: number, ia: number): void => { | |
| 211 | const n = id(iz, ia) | |
| 212 | if (seen.has(n)) return | |
| 213 | seen.add(n) | |
| 214 | if (!fits(iz * GROW_M, ia * GROW_A_M)) return | |
| 215 | const k = key(iz, ia) | |
| 216 | let lo = 0, hi = frontier.length | |
| 217 | while (lo < hi) { const m = (lo + hi) >> 1; if (frontier[m]!.k < k) lo = m + 1; else hi = m } | |
| 218 | frontier.splice(lo, 0, { iz, ia, k }) | |
| 219 | } | |
| 220 | offer(iz0, ia0) | |
| 221 | while (out.length < target && frontier.length) { | |
| 222 | const { iz, ia } = frontier.shift()! | |
| 223 | const n = id(iz, ia) | |
| 224 | if (taken.has(n)) continue | |
| 225 | taken.add(n) | |
| 226 | out.push(n) | |
| 227 | offer(iz + 1, ia); offer(iz - 1, ia); offer(iz, ia + 1); offer(iz, ia - 1) | |
| 228 | } | |
| 229 | return out | |
| 230 | } | |
| 231 | ||
| 232 | /** Light on a field now: 'on' as at noon, 'dim' (the rod's glow, by day), or 'night'. */ | |
| 233 | export type FieldLight = 'on' | 'dim' | 'night' | |
| 234 | ||
| 235 | /** | |
| 236 | * Whether Tower has a crop lit at `hourOfDay`. Its day starts at sunrise and lasts its own | |
| 237 | * `dayH`, however long the sun's day is: past sunset it is lit on into the night, and short | |
| 238 | * of it Tower takes its light away — which by day leaves the rod's glow. | |
| 239 | */ | |
| 240 | export function fieldLight(dayH: number, hourOfDay: number, sunriseH: number, sunsetH: number): FieldLight { | |
| 241 | const since = mod(hourOfDay - sunriseH, 24) | |
| 242 | if (since < dayH) return 'on' | |
| 243 | return since < sunsetH - sunriseH ? 'dim' : 'night' | |
| 244 | } | |
| 245 | ||
| 246 | const f4 = (x: number): string => x.toFixed(4) | |
| 247 | const lin = (c: readonly number[]): string => `vec3(${c.map((v) => f4((v / 255) ** 2.2)).join(', ')})` | |
| 248 | ||
| 249 | /** | |
| 250 | * The same, as GLSL: `farmField(zM, aM)` returning the crop index, the distance to the field's | |
| 251 | * edge and the rows' direction, and the crop table as constants. | |
| 252 | */ | |
| 253 | export const FARM_GLSL = /* glsl */` | |
| 254 | float farmPermute(float x) { return mod((34.0 * x + 1.0) * x, 289.0); } | |
| 255 | float farmHash(float i, float j, float salt) { | |
| 256 | return farmPermute(mod(farmPermute(mod(farmPermute(mod(i, 289.0)) + mod(j, 289.0), 289.0)) + salt, 289.0)) / 289.0; | |
| 257 | } | |
| 258 | const int CROP_COUNT = ${CROPS.length}; | |
| 259 | const float CROP_EDGE[${CROPS.length}] = float[](${CROP_EDGES.map(f4).join(', ')}); | |
| 260 | const vec3 CROP_BLOOM[${CROPS.length}] = vec3[](${CROPS.map((c) => lin(c.bloom)).join(', ')}); | |
| 261 | const vec3 CROP_LEAF[${CROPS.length}] = vec3[](${CROPS.map((c) => lin(c.leaf)).join(', ')}); | |
| 262 | const float CROP_COVER[${CROPS.length}] = float[](${CROPS.map((c) => f4(c.cover)).join(', ')}); | |
| 263 | const float CROP_ROW[${CROPS.length}] = float[](${CROPS.map((c) => f4(c.rowM)).join(', ')}); | |
| 264 | const float CROP_DAY[${CROPS.length}] = float[](${CROPS.map((c) => f4(c.dayH)).join(', ')}); | |
| 265 | const vec3 FARM_SOIL = ${lin(SOIL)}; | |
| 266 | const vec3 FARM_TRACK = ${lin(TRACK)}; | |
| 267 | int pickCrop(float h) { | |
| 268 | for (int i = 0; i < CROP_COUNT; i++) if (h < CROP_EDGE[i]) return i; | |
| 269 | return CROP_COUNT - 1; | |
| 270 | } | |
| 271 | /** Nearest two of a jittered lattice: xy the nearest cell, z the bisector's distance, w unused. */ | |
| 272 | vec4 farmBlocks(vec2 x) { | |
| 273 | vec2 n0 = floor(x); | |
| 274 | float d1 = 1e9, d2 = 1e9; | |
| 275 | vec2 c1 = n0, p1 = vec2(0.0), p2 = vec2(0.0); | |
| 276 | for (int dj = -1; dj <= 1; dj++) for (int di = -1; di <= 1; di++) { | |
| 277 | vec2 c = n0 + vec2(float(di), float(dj)); | |
| 278 | float jw = mod(c.y, ${BLOCKS_AROUND.toFixed(1)}); | |
| 279 | vec2 p = c + 0.15 + 0.7 * vec2(farmHash(c.x, jw, 11.0), farmHash(c.x, jw, 23.0)); | |
| 280 | float d = dot(p - x, p - x); | |
| 281 | if (d < d1) { d2 = d1; p2 = p1; d1 = d; p1 = p; c1 = c; } else if (d < d2) { d2 = d; p2 = p; } | |
| 282 | } | |
| 283 | return vec4(c1, dot(0.5 * (p1 + p2) - x, normalize(p2 - p1)), 0.0); | |
| 284 | } | |
| 285 | vec4 farmFields(vec2 x, vec2 b, out vec2 p1) { | |
| 286 | vec2 n0 = floor(x); | |
| 287 | float d1 = 1e9, d2 = 1e9; | |
| 288 | vec2 c1 = n0, p2 = vec2(0.0); | |
| 289 | p1 = vec2(0.0); | |
| 290 | for (int dj = -1; dj <= 1; dj++) for (int di = -1; di <= 1; di++) { | |
| 291 | vec2 c = n0 + vec2(float(di), float(dj)); | |
| 292 | vec2 k = vec2(b.x * 17.0 + c.x, b.y * 29.0 + c.y); | |
| 293 | vec2 p = c + 0.1 + 0.8 * vec2(farmHash(k.x, k.y, 13.0), farmHash(k.x, k.y, 19.0)); | |
| 294 | float d = dot(p - x, p - x); | |
| 295 | if (d < d1) { d2 = d1; p2 = p1; d1 = d; p1 = p; c1 = c; } else if (d < d2) { d2 = d; p2 = p; } | |
| 296 | } | |
| 297 | return vec4(c1, dot(0.5 * (p1 + p2) - x, normalize(p2 - p1)), 0.0); | |
| 298 | } | |
| 299 | /** x: crop, y: metres to the field's edge (negative on a track), z: metres across its rows, w: along them. */ | |
| 300 | vec4 farmField(float zM, float aM) { | |
| 301 | float a0 = mod(aM, ${CIRCUMFERENCE_M.toFixed(3)}); | |
| 302 | vec4 blk = farmBlocks(vec2(zM / ${f4(BLOCK_M)}, a0 / ${f4(BLOCK_A_M)})); | |
| 303 | vec2 b = vec2(blk.x, mod(blk.y, ${BLOCKS_AROUND.toFixed(1)})); | |
| 304 | vec2 jit = 0.15 + 0.7 * vec2(farmHash(b.x, b.y, 11.0), farmHash(b.x, b.y, 23.0)); | |
| 305 | vec2 centre = (blk.xy + jit) * vec2(${f4(BLOCK_M)}, ${f4(BLOCK_A_M)}); | |
| 306 | float blockEdge = blk.z * ${f4(Math.min(BLOCK_M, BLOCK_A_M))}; | |
| 307 | float ang = farmHash(b.x, b.y, 7.0) * 3.14159265; | |
| 308 | float stretch = 1.3 + 0.9 * farmHash(b.x, b.y, 8.0); | |
| 309 | float size = 220.0 + 220.0 * farmHash(b.x, b.y, 9.0); | |
| 310 | vec2 d = vec2(zM - centre.x, a0 - centre.y); | |
| 311 | float cs = cos(ang), sn = sin(ang); | |
| 312 | vec2 u = vec2(cs * d.x + sn * d.y, -sn * d.x + cs * d.y); | |
| 313 | vec2 p1; | |
| 314 | vec4 fl = farmFields(vec2(u.x / (size * stretch), u.y / size), b, p1); | |
| 315 | vec2 k = vec2(b.x * 17.0 + fl.x, b.y * 29.0 + fl.y); | |
| 316 | float part = 0.0, stripEdge = 1e9; | |
| 317 | if (farmHash(k.x, k.y, 31.0) < 0.35) { | |
| 318 | float n = 2.0 + floor(farmHash(k.x, k.y, 37.0) * 3.0); | |
| 319 | float t = (u.y / size - p1.y) * n + n * 0.5; | |
| 320 | part = floor(t); | |
| 321 | float f = fract(t); | |
| 322 | stripEdge = min(f, 1.0 - f) * (size / n); | |
| 323 | } | |
| 324 | float edge = min(min(blockEdge - ${f4(TRACK_M / 2)}, fl.z * size - ${f4(MARGIN_M / 2)}), stripEdge - ${f4(MARGIN_M / 3)}); | |
| 325 | if (blockEdge < ${f4(TRACK_M / 2)}) edge = min(edge, -0.01); | |
| 326 | return vec4(float(pickCrop(farmHash(k.x, k.y, 41.0 + part))), edge, u.y, u.x); | |
| 327 | } | |
| 328 | ` |