pike/val Public Public: anyone can read this; nobody can change it.
forked from benjivers/val
workbenchthis machine
Files 2e870ef8994867709476cca0f461c0ff071a8c69
| 1 | /** |
| 2 | * Wall columns, collapsed into runs. |
| 3 | * |
| 4 | * A raycaster produces one vertical slice per screen column, and the naive |
| 5 | * renderer pays one `fillRect` for each of them — a couple of thousand fills |
| 6 | * a frame on a wide canvas, most of them identical to their neighbour. The |
| 7 | * shading is quantised into levels already (`scene.js`), so a flat wall face |
| 8 | * is a stretch of columns asking for exactly the same rectangle, drawn one |
| 9 | * pixel at a time. |
| 10 | * |
| 11 | * This walks the columns once and hands back the runs: the stretches where |
| 12 | * the level, the face, the haze over it and the geometry all agree, so the |
| 13 | * caller draws one rectangle for the stretch. A corridor collapses hundreds |
| 14 | * of fills into a handful and the picture does not change. |
| 15 | * |
| 16 | * Pure data in, pure data out — no canvas here — and nothing is allocated |
| 17 | * per frame: the column arrays and the run objects are kept and rewritten. |
| 18 | */ |
| 19 | |
| 20 | /** A level of -1 is sky: no wall was struck, and no run covers it. */ |
| 21 | export const SKY = -1; |
| 22 | /** How far apart two slices may sit and still be drawn as one rectangle. */ |
| 23 | export const GEOM = 0.5; |
| 24 | /** Haze is quantised this finely before it joins the run's identity. */ |
| 25 | export const HAZE_STEPS = 16; |
| 26 | |
| 27 | /** The columns of one frame: what the ray loop writes, all of it flat. */ |
| 28 | export function makeColumns(count) { |
| 29 | return { |
| 30 | count, |
| 31 | level: new Int16Array(count).fill(SKY), |
| 32 | side: new Int8Array(count), |
| 33 | top: new Float32Array(count), |
| 34 | high: new Float32Array(count), |
| 35 | haze: new Float32Array(count), |
| 36 | }; |
| 37 | } |
| 38 | |
| 39 | const run = () => ({ from: 0, to: 0, level: SKY, side: 0, top: 0, high: 0, step: 0, haze: 0 }); |
| 40 | |
| 41 | /** The quantised haze of a column: equal steps draw as one rectangle. */ |
| 42 | export const hazeStep = (haze) => Math.round(Math.min(1, Math.max(0, haze)) * HAZE_STEPS); |
| 43 | |
| 44 | /** |
| 45 | * Write the runs of `cols` into `out`, growing it as needed, and return how |
| 46 | * many of its entries are this frame's. `out[i].from` and `.to` are column |
| 47 | * indices, both inclusive. |
| 48 | */ |
| 49 | export function wallRuns(cols, out) { |
| 50 | let used = 0; |
| 51 | let open = false; // is out[used - 1] still growing, or did sky end it? |
| 52 | for (let i = 0; i < cols.count; i++) { |
| 53 | const level = cols.level[i]; |
| 54 | if (level === SKY) { |
| 55 | open = false; |
| 56 | continue; |
| 57 | } |
| 58 | const step = hazeStep(cols.haze[i]); |
| 59 | const top = cols.top[i]; |
| 60 | const high = cols.high[i]; |
| 61 | const last = used > 0 ? out[used - 1] : null; |
| 62 | if ( |
| 63 | open && |
| 64 | last.level === level && |
| 65 | last.side === cols.side[i] && |
| 66 | last.step === step && |
| 67 | Math.abs(last.top - top) <= GEOM && |
| 68 | Math.abs(last.high - high) <= GEOM |
| 69 | ) { |
| 70 | last.to = i; |
| 71 | continue; |
| 72 | } |
| 73 | if (out.length <= used) out.push(run()); |
| 74 | const fresh = out[used]; |
| 75 | fresh.from = i; |
| 76 | fresh.to = i; |
| 77 | fresh.level = level; |
| 78 | fresh.side = cols.side[i]; |
| 79 | fresh.top = top; |
| 80 | fresh.high = high; |
| 81 | fresh.step = step; |
| 82 | fresh.haze = step / HAZE_STEPS; |
| 83 | used++; |
| 84 | open = true; |
| 85 | } |
| 86 | return used; |
| 87 | } |
| 88 | |
| 89 | /** How many columns the runs cover: the picture's width, for a test to check. */ |
| 90 | export function covered(out, used) { |
| 91 | let total = 0; |
| 92 | for (let i = 0; i < used; i++) total += out[i].to - out[i].from + 1; |
| 93 | return total; |
| 94 | } |