code wiki / (root) / nx_fluid.nx

nx_fluid.nx source

↩ module page · 190 lines · 8631 B

1// nx_fluid.nx -- ★R6 of the Infinigen ladder: FLUID SIMULATION (their FLIP-fluids axis). A height-field WAVE 2// simulation (the shallow-water / wave-equation grid -- the standard real-time water technique; FLIP is the 3// particle variant). Real physics, all integer: a height grid h + velocity grid v integrated by the discrete 4// wave equation v += c^2*dt * laplacian(h) ; v *= damp ; h += v , with REFLECTING boundaries so VOLUME is 5// exactly conserved (sum(laplacian)=0 => sum(v) stays 0 => sum(h) constant). Disturbances propagate outward at 6// finite speed, reflect off shores, and damp -- not a flat plane. license_tier: ORIGINAL 7import "nx_syscalls.nx" 8import "nx_itrig.nx" 9import "nx_vecmath.nx" 10const FL_MAGIC_1024: i64 = 1024 11const FL_MAGIC_1048576: i64 = 1048576 12const FL_MAGIC_9973: i64 = 9973 13const FL_MAGIC_4096: i64 = 4096 14const FL_MAGIC_65536: i64 = 65536 15 16const FL_N: i64 = 168 // grid cells per axis 17const FL_K: i64 = 100 // c^2*dt (fx1024) -- CFL-stable (4*K/1024 = 0.39, well under the 2D limit) 18const FL_DAMP: i64 = 1013 // velocity damping per step (~0.989) -- removes energy, keeps sum(v)=0 19static FL_H: i64 // height grid ptr (FL_N*FL_N i64, fx) 20static FL_V: i64 // velocity grid ptr 21static FL_STEP: i64 // step counter (rotates the conservation-remainder offset) 22 23func fl_isqrt(x: i64) -> i64 { return vm_isqrt(x) } 24func fl_iabs(v: i64) -> i64 { if v<0 {return 0-v} return v } 25func fl_clampi(v: i64, lo: i64, hi: i64) -> i64 { if v<lo {return lo} if v>hi {return hi} return v } 26 27func fl_init() -> i64 { 28 if FL_H == 0 { FL_H = sys_mmap(FL_N*FL_N*8) as i64 } 29 if FL_V == 0 { FL_V = sys_mmap(FL_N*FL_N*8) as i64 } 30 let h: *i64 = FL_H as *i64 31 let v: *i64 = FL_V as *i64 32 var i: i64 = 0 33 while i < FL_N*FL_N { h[i]=0; v[i]=0; i=i+1 } 34 FL_STEP = 0 35 return 0 36} 37 38// place a smooth circular disturbance (a raindrop / splash) of amplitude amp at (cx,cy), radius r. 39func fl_drop(cx: i64, cy: i64, r: i64, amp: i64) -> i64 { 40 let h: *i64 = FL_H as *i64 41 var y: i64 = cy - r 42 while y <= cy + r { 43 var x: i64 = cx - r 44 while x <= cx + r { 45 if x >= 0 { if x < FL_N { if y >= 0 { if y < FL_N { 46 let d2: i64 = (x-cx)*(x-cx) + (y-cy)*(y-cy) 47 if d2 < r*r { 48 // cosine-ish bump: amp * (1 + cos(pi*d/r))/2 via (r^2-d^2)/r^2 squared, smooth 49 let f: i64 = (r*r - d2)*FL_MAGIC_1024/(r*r) 50 h[y*FL_N+x] = h[y*FL_N+x] + amp*f*f/FL_MAGIC_1048576 51 } 52 } } } } 53 x = x + 1 54 } 55 y = y + 1 56 } 57 return 0 58} 59 60// one wave-equation step. REFLECTING boundaries (out-of-grid neighbour = centre) => volume-conserving. 61func fl_step() -> i64 { 62 let h: *i64 = FL_H as *i64 63 let v: *i64 = FL_V as *i64 64 // pass 1: v += K * laplacian(h) ; v *= damp (reads h only) 65 var y: i64 = 0 66 while y < FL_N { 67 var x: i64 = 0 68 while x < FL_N { 69 let c: i64 = h[y*FL_N+x] 70 var hl: i64 = c 71 var hr: i64 = c 72 var hu: i64 = c 73 var hd: i64 = c 74 if x > 0 { hl = h[y*FL_N+x-1] } 75 if x < FL_N-1 { hr = h[y*FL_N+x+1] } 76 if y > 0 { hu = h[(y-1)*FL_N+x] } 77 if y < FL_N-1 { hd = h[(y+1)*FL_N+x] } 78 let lap: i64 = hl + hr + hu + hd - 4*c 79 var nv: i64 = v[y*FL_N+x] + lap*FL_K/FL_MAGIC_1024 80 nv = nv*FL_DAMP/FL_MAGIC_1024 81 v[y*FL_N+x] = nv 82 x = x + 1 83 } 84 y = y + 1 85 } 86 // ★conservation projection: integer truncation biases the per-cell v-update so sum(v) drifts off 0 (which 87 // would slowly change total volume). Subtract the mean velocity -> sum(v)=0 exactly (removes the spurious 88 // uniform-drift mode; real waves, whose mean is ~0, are untouched). Guarantees mass conservation. 89 var vs: i64 = 0 90 var k: i64 = 0 91 while k < FL_N*FL_N { vs = vs + v[k]; k = k + 1 } 92 let vm: i64 = vs / (FL_N*FL_N) 93 let rem: i64 = vs - vm*(FL_N*FL_N) // the truncation leftover (same sign as vs) 94 // remove the mean from every cell (sum(v) now == rem) 95 var i: i64 = 0 96 while i < FL_N*FL_N { v[i] = v[i] - vm; i = i + 1 } 97 // zero the leftover EXACTLY by distributing -+1 to |rem| cells -- but at a ROTATING offset each step so the 98 // corrections spread uniformly over time (a fixed offset would pump energy into one region). sum(v)=0 exactly. 99 FL_STEP = FL_STEP + 1 100 if rem != 0 { 101 let off: i64 = (FL_STEP*FL_MAGIC_9973) % (FL_N*FL_N) 102 var cnt: i64 = rem 103 var dir: i64 = 0 - 1 104 if rem < 0 { cnt = 0 - rem; dir = 1 } 105 var c2: i64 = 0 106 while c2 < cnt { 107 let idx: i64 = (off + c2) % (FL_N*FL_N) 108 v[idx] = v[idx] + dir 109 c2 = c2 + 1 110 } 111 } 112 // pass 2: h += v 113 i = 0 114 while i < FL_N*FL_N { h[i] = h[i] + v[i]; i = i + 1 } 115 return 0 116} 117 118func fl_h(x: i64, y: i64) -> i64 { let h: *i64 = FL_H as *i64; return h[y*FL_N+x] } 119func fl_n() -> i64 { return FL_N } 120// total signed volume (conserved) and total abs-energy (damps) 121func fl_volume() -> i64 { let h: *i64 = FL_H as *i64; var s: i64=0; var i: i64=0; while i<FL_N*FL_N { s=s+h[i]; i=i+1 } return s } 122func fl_energy() -> i64 { let h: *i64 = FL_H as *i64; var s: i64=0; var i: i64=0; while i<FL_N*FL_N { s=s+fl_iabs(h[i]); i=i+1 } return s } 123// peak amplitude max|h| -- the clean damping signal (decays from spreading + damping; unlike sum|h| which 124// grows as the wave develops troughs) 125func fl_maxabs() -> i64 { let h: *i64 = FL_H as *i64; var m: i64=0; var i: i64=0; while i<FL_N*FL_N { let a: i64=fl_iabs(h[i]); if a>m {m=a} i=i+1 } return m } 126// wavefront radius from (cx,cy): farthest disturbed cell (|h|>thr) 127func fl_front(cx: i64, cy: i64, thr: i64) -> i64 { 128 let h: *i64 = FL_H as *i64 129 var best: i64 = 0 130 var y: i64 = 0 131 while y < FL_N { 132 var x: i64 = 0 133 while x < FL_N { 134 if fl_iabs(h[y*FL_N+x]) > thr { 135 let d: i64 = fl_isqrt((x-cx)*(x-cx)+(y-cy)*(y-cy)) 136 if d > best { best = d } 137 } 138 x = x + 1 139 } 140 y = y + 1 141 } 142 return best 143} 144 145// render the height field as a shaded water surface into fb (W*H): normal from the height gradient, sun diffuse 146// + specular glint + deep-water base + sky tint. Ripples read as light/dark bands. sun = fx1024 unit dir. 147func fl_render(fb: *i64, W: i64, H: i64, sunx: i64, suny: i64, sunz: i64) -> i64 { 148 let h: *i64 = FL_H as *i64 149 let sl: i64 = fl_isqrt(sunx*sunx+suny*suny+sunz*sunz) 150 let lx: i64 = sunx*FL_MAGIC_1024/sl 151 let ly: i64 = suny*FL_MAGIC_1024/sl 152 let lz: i64 = sunz*FL_MAGIC_1024/sl 153 var py: i64 = 0 154 while py < H { 155 var px: i64 = 0 156 while px < W { 157 let cx: i64 = fl_clampi(px*FL_N/W, 1, FL_N-2) 158 let cy: i64 = fl_clampi(py*FL_N/H, 1, FL_N-2) 159 let dhx: i64 = h[cy*FL_N+cx+1] - h[cy*FL_N+cx-1] 160 let dhy: i64 = h[(cy+1)*FL_N+cx] - h[(cy-1)*FL_N+cx] 161 // surface normal (height scaled down so slopes are gentle): (-dhx, S, -dhy) 162 var nx: i64 = 0 - dhx*4 163 var ny: i64 = FL_MAGIC_4096 164 var nz: i64 = 0 - dhy*4 165 let nl: i64 = fl_isqrt(nx*nx+ny*ny+nz*nz) 166 if nl > 0 { nx=nx*FL_MAGIC_1024/nl; ny=ny*FL_MAGIC_1024/nl; nz=nz*FL_MAGIC_1024/nl } 167 var nd: i64 = (nx*lx+ny*ly+nz*lz)/FL_MAGIC_1024 168 if nd < 0 { nd = 0 } 169 // specular: reflect the view (straight down, 0,1,0) not used; use half-vector with view=up 170 let hx: i64 = lx 171 let hyv: i64 = ly + FL_MAGIC_1024 172 let hz: i64 = lz 173 let hl2: i64 = fl_isqrt(hx*hx+hyv*hyv+hz*hz) 174 var sp: i64 = (nx*hx+ny*hyv+nz*hz)/hl2 175 if sp < 0 { sp = 0 } 176 sp = sp*sp/FL_MAGIC_1024; sp = sp*sp/FL_MAGIC_1024; sp = sp*sp/FL_MAGIC_1024 // ^8 tight glint 177 // deep-water base + sky tint by upward-facing + diffuse 178 let lit: i64 = 40 + nd*70/FL_MAGIC_1024 179 var r: i64 = 26*lit/100 + ny*24/FL_MAGIC_1024 + sp*200/FL_MAGIC_1024 180 var g: i64 = 78*lit/100 + ny*30/FL_MAGIC_1024 + sp*205/FL_MAGIC_1024 181 var b: i64 = 128*lit/100 + ny*36/FL_MAGIC_1024 + sp*210/FL_MAGIC_1024 182 if r>255{r=255} if g>255{g=255} if b>255{b=255} 183 if r<0{r=0} if g<0{g=0} if b<0{b=0} 184 fb[py*W+px] = r + g*256 + b*FL_MAGIC_65536 185 px = px + 1 186 } 187 py = py + 1 188 } 189 return 0 190}