tower / files / src / terrain / farms.ts
farms.ts
| 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 | ` |