reFlow
- 3 Devlogs
- 5 Total hours
a in-browser 3d CFD simulation site that I made on a whim
a in-browser 3d CFD simulation site that I made on a whim
reFlow Devlog 4 — pain & suffering++
long one, buckle up. also never posted devlog 3, so some of that’s folded in here.
hour 13 (retroactive): hardware hijacking
wanted the tunnel to interrogate your PC. hooked into the WebGL debug renderer extension for real GPU model strings (had to regex out ANGLE’s prefix junk), navigator.hardwareConcurrency for core count, performance.memory for live JS heap, and the Battery API for charge state. built a perf tracker timing the physics loop per frame — if it blows the 16ms budget, System Stress flips to CRITICAL with specific diagnoses like “Low Core Alert” instead of just “it’s lagging.”
hour 17: the great physics divorce
the entire physics engine was crammed inside Three.js’s render loop in WindTunnel.tsx. worked at 10k particles, died at 30k+ — browser froze, laptop got loud. ripped every physics function into a standalone physics.ts — pure math, zero Three.js imports, just numbers in and out. took two hours of replacing Vector3.set() calls with raw tuples and hand-rolled rotation matrices.
hour 18: web workers enter the chat
moved the whole particle loop off the main thread into a Web Worker via Vite’s one-line worker syntax. main thread sends position/velocity/color arrays, worker crunches 40k particles, sends them back. UI never freezes. used Transferable objects so arrays move by ownership transfer instead of getting copied every frame. added a main-thread fallback if workers fail to load.
hour 19: the wake was lying to me
old turbulence was just Math.sin() — predictable, periodic, fake. replaced with hash-based pseudo-random noise for organic chaos, added von Kármán vortex shedding behind bluff bodies (alternating vortex pairs, like lampposts oscillating in wind), and a no-slip boundary layer so particles slow down near surfaces instead of just bouncing.
hour 20: the car was ugly and the flat plate was a brick
car was a 2D extruded polygon that looked like a cheese wedge. rebuilt with bezier/quadratic curves for a real windshield, roofline, and spoiler lip, plus wheel arch cutouts. flat plate was basically a cutting board (BoxGeometry(0.3, 5.8, 5.8)) — thinned to 0.08 with beveled edges so it actually reads as a plate.
hour 21: torus goes brrr
torus was hardcoded. added live sliders for major radius (1.5–6.0) and minor radius (0.3–2.5) that rebuild the geometry AND feed the physics engine in real time. thin ring = ethereal vortex shedding, chonky donut = half-blocked tunnel. added an R/r ratio readout because it sounds smart.
hour 22: the sidebar was eating my screen
between left controls, right diagnostics, a bottom chart/AI panel, and a header full of badges, the 3D canvas shrank to a postage stamp. added collapsible panels — chevron toggles collapse the sidebars to thin strips and the canvas reclaims ~80% of the screen. sometimes the best feature is removing stuff.
hour 23: the absolute madman feature (GLB loader)
end of devlog 3 I said “maybe a glTF loader, we’ll see.” we saw. you can now upload any .glb/.gltf, and the app auto-scales/centers it, then voxelizes it — shoots rays through a 48×48×48 grid, counts intersections, and marks solid vs. air. physics engine uses that voxel grid for collisions. not physically perfect, but you can drop in a Stanford bunny and watch particles flow around its ears.
file count went from 2 source files to 5. physics.ts alone is 731 lines of math. wrote more linear algebra this week than my whole school career.
up next: markdown rendering for AI reports, and fixing the README’s leftover “YOUR_USERNAME” placeholders before anyone notices.
reFlow Devlog #2 - moar suffering!!!!
round two. we are back. the wind tunnel is no longer just a 3d playground; it’s a quantum control deck. here is how i spent the last few hours losing sleep over perspective cameras, slice projection math, and custom state toggle controls.
hour 7: the flatland incursion (toggling 2d slice mode)
why turn a 3d tunnel back to 2d? because aerodynamics is all about cross-sections.
how to constrain a 3d particle swarm to 2d without a total refactor: when “is2D” is true, we hijack the particle spawner. instead of spreading particles across x-axis depth, we force positions[idx] = (Math.random() - 0.5) * 0.1, wrapping the flow into a super thin sheet at x = 0.
we also restricted the smoke line emitters to a vertical slice. the result looks like a clean laser-cut sheet of wind passing through the wing. (not really, but it’s the thought that counts)
hour 8: camera capture and control hijacking
particles are in 2d, but the camera still rotates in 3d. rotating it breaks the illusion (just a flat sheet of dots).
solution: camera hijacking.
when 2d mode triggers, we fly the camera to (38, 0, 0) looking down the x-axis at the wing. we disable OrbitControls rotation (controls.enableRotate = false) and force camera.up.set(0, 1, 0).
debugging this was a nightmare—OrbitControls flips the camera if target/up vectors aren’t updated first. now, swapping dimensions is smooth.
hour 9: freezing time (the pause engine)
sometimes wind moves too fast to see gradients. we need a freeze-frame.
added “isPaused” state.
first try: skip the animate loop. but that froze the rendering, stopping camera rotation.
second try: keep the rendering loop active, but bypass particle position updates.
now you can pause, orbit in 3d, and inspect smoke streams and vectors suspended in amber. sci-fi vibes.
hour 10: tuning the storm (particle customizability)
default 6,000 particles is cool, but my GPU was barely breaking a sweat.
expanded particle count up to 20,000 (step 1000) and added a size slider (0.1 to 1.5).
adjusting particle size from tiny dust specks to fat plasma spheres is satisfying. setting count to 20,000 makes the wind tunnel look like a dense gas nebula. the cpu is sweating, but performance is smooth thanks to typed buffer array updates.
hour 11: cyberpunk telemetry deck (hud overhaul)
with new toggleable states (2d/3d and pause/play), the dashboard needed a facelift.
built a cyber-cyan “Switch 2D” button with custom ambient glow shadows (shadow-[0_0_15px_rgba(125,249,255,0.15)]) and a yellow pause button changing to play icon.
added a footer telemetry status pill showing “2D Slice | [Active Object]” vs “3D Space | [Active Object]”. makes the app feel like a high-budget engineering interface.
hour 12: collision math strikes back (local normals)
particles clipped through spherical and box obstacles due to lazy velocity mirroring.
introduced a robust getNormalLocal(type, x, y, z) helper.
when a particle collides with a sphere, we calculate the exact radial normal: (x/r, y/r, z/r). for box geometry, the helper evaluates which face was breached (min === dx logic) and returns an axis normal.
now particles bounce and slide off physical bodies with realistic deflection vectors. no more ghosting.
next up: actually making the visuals work (someone send help)
reFlow Devlog 1 - building a 3d wind tunnel without losing my mind (?)
enjoy my suffering.
hour 1: the “it’s not that hard” phase
decided on: react, three.js, and tailwind css.
getting three.js to render took half an hour because i forgot lights. stared at a pitch black void wondering why my life is like this. finally got a grey cube spinning. les go spinny coob
hour 2: wikipedia math is scary
goal: make an airfoil (wing shape).
searched “naca airfoil math formula” and got hit with an abomination of an equation: yt = 5 * t * (0.2969 * sqrt(x) - 0.1260 * x - 0.3516 * x^2 + 0.2843 * x^3 - 0.1015 * x^4). wouldn’t have signed up if i knew math was involved.
translated it into a javascript loop generating a THREE.Shape. my eyes watered staring at parentheses. messed up the loop direction 3 times and made twisted metal instead of a wing. had to scale it up cuz default was microscopic. after 4 tries, it looks like a wing. LETS GOOO
hour 3: the wind has to blow
made a particle system with 6,000 points. spawn at front, move back, respawn.
easy, but particles must bend around the wing. wrote a collision loop.
first draft: particles got stuck, formed a massive blue blob.
second draft: calculated slope. particles flew off at 90-degree angles.
coded a simplified double-flow vector calc. not real CFD (my laptop would explode running Navier-Stokes), but it looks real.
hour 4: adding cool dashboards
app needed to look like a telemetry deck. added:
sidebar with sliders for viscosity, inlet speed, angle of attack.
custom html5 canvas real-time graph. draws lift (cyan) and drag (orange) lines. legit math at home guys.
pressure heatmap. flat vertical plane in center. slow air hitting nose = high pressure (RED). fast air over top = low pressure (NEON GREEN). tilting the wing shifts the red spot. ASMR for the eyes wooooo
hour 5: stall warning and ai pilot
added stall physics! over 15deg tilt, lift cuts by 70% and drag multiplies. added a flashing CRITICAL STALL WARNING banner.
integrated Gemini API for aero reports then realized this comp requires no auth, so api keys are prolly an issue.
wrote a local backup expert system. no API key = runs generateLocalReport() with rule-based templates. 100% plug-and-play.
hour 6: single-file challenge
wanted app super portable. one .html file.
vite/tailwind output to .js and .css. wrote bundle.js to inline them which ended up giving
“ReferenceError: require is not defined in ES module scope.” bruh. renamed it to bundle.cjs and that worked yay
btw throwing html into google AI studio gets a free hosted site?? the more you know i guess
lots of math still broken lol.
coolest project i’ve built though. going to sleep for a week.