code wiki / (root) / nx_fluid.nx

nx_fluid.nx source

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