numerics
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1/// @file main.cpp
2/// @brief 3D SPH fluid simulation -- hose demo with scene rotation
3///
4/// Two water jets aimed at the centre of the domain:
5/// H -- toggle hot hose (top-left corner, 80 degC)
6/// C -- toggle cold hose (top-right corner, 5 degC)
7///
8/// Arrow keys orbit the camera AND physically tilt the scene so that gravity
9/// always points along the camera's "down" direction. Particles therefore
10/// fall toward whichever wall is currently at the bottom of the screen.
11///
12/// Controls:
13/// H / C -- hot / cold hose on-off
14/// Arrow keys -- rotate scene (gravity tilts with pitch)
15/// SPACE -- pause / resume
16/// R -- reset (clear particles, keep hose state)
17/// + / - -- more / fewer substeps per frame
18/// ESC -- quit
19
20#include "raylib.h"
21#include "raymath.h"
22#include "fluid3d.hpp"
23#include <cmath>
24#include <algorithm>
25
26static constexpr float kPI = 3.14159265f;
27static constexpr float kG = 9.81f;
28
29// --- colour ------------------------------------------------------------------
30
31static Color temp_color(float T, float T_min, float T_max) {
32 float t = (T - T_min) / (T_max - T_min + 1e-4f);
33 t = (t < 0.0f) ? 0.0f : (t > 1.0f ? 1.0f : t);
34 return ColorFromHSV((1.0f - t) * 240.0f, 1.0f, 0.95f);
35}
36
37// --- solver factory ----------------------------------------------------------
38
39static physics::FluidParams3D make_params() {
41 p.h = 0.05f;
42 p.rho0 = 1000.0f;
43 p.gamma = 7;
44 p.c0 = 10.0f;
45 p.mu = 10.0f;
46 p.mass = p.rho0 * (0.8f * p.h) * (0.8f * p.h) * (0.8f * p.h);
47 p.gx = 0.0f; p.gy = -kG; p.gz = 0.0f;
48 p.dt = 0.001f;
49 p.xmin = p.ymin = p.zmin = 0.0f;
50 p.xmax = p.ymax = p.zmax = 0.8f;
51 p.restitution = 0.01f;
52 p.alpha_T = 0.005f;
53 p.h_conv = 8.0f;
54 return p;
55}
56
57// --- vector helpers (raylib Vector3 doesn't have all we need) ----------------
58
59static Vector3 v3_norm(Vector3 v) {
60 float len = sqrtf(v.x*v.x + v.y*v.y + v.z*v.z);
61 if (len < 1e-8f) return {0.0f, 1.0f, 0.0f};
62 return {v.x/len, v.y/len, v.z/len};
63}
64static Vector3 v3_cross(Vector3 a, Vector3 b) {
65 return {a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x};
66}
67
68// --- Hose --------------------------------------------------------------------
69
70struct Hose {
71 Vector3 source;
72 Vector3 dir; ///< normalised jet direction
73 Vector3 perp_u; ///< two axes spanning the spray cone
74 Vector3 perp_v;
76 float speed;
77 float cone_half; ///< half-angle in radians
78 bool active = false;
79
80 void init(Vector3 src, Vector3 target,
81 float temp, float spd, float half_angle) {
82 source = src;
83 temperature = temp;
84 speed = spd;
85 cone_half = half_angle;
86
87 dir = v3_norm({target.x - src.x, target.y - src.y, target.z - src.z});
88
89 // Build orthonormal basis for the cone
90 Vector3 ref = (fabsf(dir.y) < 0.9f) ? Vector3{0,1,0} : Vector3{1,0,0};
91 perp_u = v3_norm(v3_cross(dir, ref));
92 perp_v = v3_cross(dir, perp_u); // already unit since dir ⊥ perp_u
93 }
94
95 void emit(physics::FluidSolver3D& solver, int count,
96 int max_particles = 1800) const {
97 if (!active) return;
98 if ((int)solver.particles().size() >= max_particles) return;
99
100 const float max_r = tanf(cone_half);
101 for (int i = 0; i < count; ++i) {
102 float angle = (float)GetRandomValue(0, 10000) / 10000.0f * 2.0f * kPI;
103 float r = (float)GetRandomValue(0, 10000) / 10000.0f * max_r;
104 float su = cosf(angle) * r;
105 float sv = sinf(angle) * r;
106
107 // Spray direction with random cone spread
108 Vector3 d = v3_norm({
109 dir.x + su * perp_u.x + sv * perp_v.x,
110 dir.y + su * perp_u.y + sv * perp_v.y,
111 dir.z + su * perp_u.z + sv * perp_v.z
112 });
113
114 // Small position jitter so particles don't all stack
115 float jx = (float)(GetRandomValue(-100, 100)) * 0.00008f;
116 float jy = (float)(GetRandomValue(-100, 100)) * 0.00008f;
117 float jz = (float)(GetRandomValue(-100, 100)) * 0.00008f;
118
119 solver.add_particle(source.x + jx, source.y + jy, source.z + jz,
120 d.x * speed, d.y * speed, d.z * speed,
122 }
123 }
124
125 // Visual nozzle position
126 Vector3 pos() const { return source; }
127};
128
129// --- main --------------------------------------------------------------------
130
131int main() {
132 InitWindow(1200, 800, "SPH 3D -- Hose Demo");
133 SetTargetFPS(60);
134
135 physics::FluidParams3D params = make_params();
136 physics::FluidSolver3D solver(params);
137 // Start empty -- hoses fill the scene
138
139 // Scene/camera orbit angles
140 // yaw: horizontal rotation around world-Y (left/right arrows)
141 // pitch: elevation angle (up/down arrows)
142 //
143 // Gravity follows camera "down": g = G * (sinY*sinP, -cosP, cosY*sinP)
144 // so pitching physically tilts the container -- pure yaw (vertical rotation)
145 // leaves gravity unchanged, which is correct.
146 float cam_yaw = kPI * 0.75f; // 135 deg -- starts from +X+Z corner (matches old view)
147 float cam_pitch = 0.45f; // ~26 deg elevation
148 const float CAM_DIST = 2.1f;
149 const float ROT_SPEED = 1.6f; // rad/s
150 const Vector3 center = {0.4f, 0.4f, 0.4f};
151
152 // Cold hose: top-right-front corner -> centre
153 Hose cold_hose, hot_hose;
154 cold_hose.init({0.77f, 0.76f, 0.02f}, center, 5.0f, 1.8f, kPI / 36.0f); // 5 deg
155 // Hot hose: top-left-front corner -> centre
156 hot_hose .init({0.03f, 0.76f, 0.02f}, center, 80.0f, 1.8f, kPI / 36.0f);
157
158 Camera3D camera{};
159 camera.fovy = 50.0f;
160 camera.projection = CAMERA_PERSPECTIVE;
161 camera.target = center;
162
163 auto apply_camera = [&]() {
164 float cp = cosf(cam_pitch), sp = sinf(cam_pitch);
165 float cy = cosf(cam_yaw), sy = sinf(cam_yaw);
166 camera.position = {
167 center.x + CAM_DIST * cp * sy,
168 center.y + CAM_DIST * sp,
169 center.z + CAM_DIST * cp * cy
170 };
171 // Camera up = scene's local Y axis (derived from orbit angles)
172 camera.up = { -sy * sp, cp, -cy * sp };
173 };
174
175 auto apply_gravity = [&]() {
176 float cp = cosf(cam_pitch), sp = sinf(cam_pitch);
177 float cy = cosf(cam_yaw), sy = sinf(cam_yaw);
178 auto& pm = solver.params_mut();
179 pm.gx = kG * sy * sp;
180 pm.gy = -kG * cp;
181 pm.gz = kG * cy * sp;
182 };
183
184 apply_camera();
185 apply_gravity();
186
187 bool paused = false;
188 int substeps = 4;
189
190 while (!WindowShouldClose()) {
191 const float dt_frame = GetFrameTime();
192
193 // -- input ---------------------------------------------------------
194 if (IsKeyPressed(KEY_SPACE)) paused = !paused;
195 if (IsKeyPressed(KEY_R)) solver.clear();
196 if (IsKeyPressed(KEY_H)) hot_hose.active = !hot_hose.active;
197 if (IsKeyPressed(KEY_C)) cold_hose.active = !cold_hose.active;
198
199 if (IsKeyPressed(KEY_EQUAL) || IsKeyPressed(KEY_KP_ADD))
200 substeps = std::min(substeps + 1, 20);
201 if (IsKeyPressed(KEY_MINUS) || IsKeyPressed(KEY_KP_SUBTRACT))
202 substeps = std::max(substeps - 1, 1);
203
204 // Arrow keys: rotate scene
205 bool rotated = false;
206 if (IsKeyDown(KEY_LEFT)) { cam_yaw -= ROT_SPEED * dt_frame; rotated = true; }
207 if (IsKeyDown(KEY_RIGHT)) { cam_yaw += ROT_SPEED * dt_frame; rotated = true; }
208 if (IsKeyDown(KEY_UP)) { cam_pitch += ROT_SPEED * dt_frame; rotated = true; }
209 if (IsKeyDown(KEY_DOWN)) { cam_pitch -= ROT_SPEED * dt_frame; rotated = true; }
210 cam_pitch = (cam_pitch < -kPI * 0.44f) ? -kPI * 0.44f
211 : (cam_pitch > kPI * 0.44f) ? kPI * 0.44f : cam_pitch;
212
213 if (rotated) { apply_camera(); apply_gravity(); }
214
215 // -- physics -------------------------------------------------------
216 if (!paused) {
217 cold_hose.emit(solver, 2);
218 hot_hose .emit(solver, 2);
219 for (int s = 0; s < substeps; ++s)
220 solver.step();
221 }
222
223 // -- render --------------------------------------------------------
224 const float T_min = solver.min_temp();
225 const float T_max = solver.max_temp();
226
227 BeginDrawing();
228 ClearBackground(Color{18, 18, 28, 255});
229 BeginMode3D(camera);
230
231 // Domain box
232 DrawCubeWires({0.4f, 0.4f, 0.4f}, 0.8f, 0.8f, 0.8f, Color{60, 60, 80, 200});
233
234 // Nozzles (always drawn so user can see where they are)
235 DrawSphere(cold_hose.pos(), 0.022f, cold_hose.active ? SKYBLUE : Color{40,40,100,180});
236 DrawSphere(hot_hose .pos(), 0.022f, hot_hose.active ? ORANGE : Color{100,40,20,180});
237
238 // Particles
239 for (const physics::Particle3D& p : solver.particles()) {
240 Color c = temp_color(p.temperature, T_min, T_max);
241 DrawSphere({p.x, p.y, p.z}, 0.016f, c);
242 }
243
244 EndMode3D();
245
246 // -- HUD -----------------------------------------------------------
247 DrawFPS(10, 10);
248 DrawText(TextFormat("Particles: %d / 1800", (int)solver.particles().size()),
249 10, 36, 20, WHITE);
250 DrawText(TextFormat("Substeps/frame: %d (%.2fx realtime)",
251 substeps, substeps * solver.params().dt * 60.0f),
252 10, 60, 18, LIGHTGRAY);
253 DrawText(TextFormat("T range: %.1f - %.1f degC", T_min, T_max),
254 10, 84, 18, LIGHTGRAY);
255
256 // Gravity direction indicator
257 const auto& pm = solver.params();
258 DrawText(TextFormat("g = (%.2f, %.2f, %.2f) m/s^2", pm.gx, pm.gy, pm.gz),
259 10, 108, 16, Color{160, 160, 160, 255});
260
261 // Hose status
262 DrawText("H = HOT hose:", 10, 136, 18, LIGHTGRAY);
263 DrawText(hot_hose.active ? "ON" : "off",
264 170, 136, 18, hot_hose.active ? ORANGE : GRAY);
265 DrawText("C = COLD hose:", 10, 158, 18, LIGHTGRAY);
266 DrawText(cold_hose.active ? "ON" : "off",
267 170, 158, 18, cold_hose.active ? SKYBLUE : GRAY);
268
269 if (paused) {
270 DrawRectangle(0, 0, 1200, 800, Color{0, 0, 0, 100});
271 DrawText("PAUSED", 1200/2 - 60, 800/2 - 20, 40, YELLOW);
272 }
273
274 // Controls bar
275 DrawRectangle(0, 770, 1200, 30, Color{0, 0, 0, 180});
276 DrawText("H=hot C=cold Arrows=rotate scene SPACE=pause R=reset +/-=speed ESC=quit",
277 8, 778, 14, Color{180, 180, 180, 255});
278
279 EndDrawing();
280 }
281
282 CloseWindow();
283 return 0;
284}
float min_temp() const
Definition fluid3d.hpp:60
void step()
Advance by one timestep. Dispatches to seq or omp backends.
Definition fluid3d.cpp:47
FluidParams3D & params_mut()
Definition fluid3d.hpp:59
const std::vector< Particle3D > & particles() const
Definition fluid3d.hpp:55
float max_temp() const
Definition fluid3d.hpp:61
const FluidParams3D & params() const
Definition fluid3d.hpp:58
void add_particle(float x, float y, float z, float vx, float vy, float vz, float T)
Definition fluid3d.cpp:29
int main()
Definition main.cpp:84
3D WCSPH fluid solver – public interface and dispatch hub
Definition main.cpp:70
bool active
Definition main.cpp:78
float temperature
Definition main.cpp:75
Vector3 source
Definition main.cpp:71
void emit(physics::FluidSolver3D &solver, int count, int max_particles=1800) const
Definition main.cpp:95
Vector3 perp_v
Definition main.cpp:74
void init(Vector3 src, Vector3 target, float temp, float spd, float half_angle)
Definition main.cpp:80
float speed
Definition main.cpp:76
float cone_half
half-angle in radians
Definition main.cpp:77
Vector3 dir
normalised jet direction
Definition main.cpp:72
Vector3 perp_u
two axes spanning the spray cone
Definition main.cpp:73
Vector3 pos() const
Definition main.cpp:126
float mu
Dynamic viscosity [Pa*s].
Definition fluid3d.hpp:26
float c0
Speed of sound [m/s].
Definition fluid3d.hpp:25
float alpha_T
Thermal diffusivity [m^2/s].
Definition fluid3d.hpp:37
float h
Smoothing length [m].
Definition fluid3d.hpp:22
float h_conv
Convective coefficient with rigid bodies [1/s].
Definition fluid3d.hpp:38
int gamma
Tait EOS exponent.
Definition fluid3d.hpp:24
float mass
Particle mass [kg] (~= rho_0*(0.8h)^3)
Definition fluid3d.hpp:27
float rho0
Rest density [kg/m^3].
Definition fluid3d.hpp:23
3D SPH particle – AoS layout
Definition particle3d.hpp:8