From 2e152217862e2156e5d73284dcb0fde6cf0b56fe Mon Sep 17 00:00:00 2001
From: Yuwei Xiao <xyw1105@126.com>
Date: Wed, 22 Apr 2020 10:05:44 +0800
Subject: [PATCH] ex5 fluid

---
 5_fluid/CMakeLists.txt |  43 ++++++
 5_fluid/FluidSim.cpp   | 188 ++++++++++++++++++++++++++
 5_fluid/FluidSim.h     | 296 +++++++++++++++++++++++++++++++++++++++++
 5_fluid/main.cpp       |  78 +++++++++++
 CMakeLists.txt         |   5 +-
 5 files changed, 608 insertions(+), 2 deletions(-)
 create mode 100644 5_fluid/CMakeLists.txt
 create mode 100644 5_fluid/FluidSim.cpp
 create mode 100644 5_fluid/FluidSim.h
 create mode 100644 5_fluid/main.cpp

diff --git a/5_fluid/CMakeLists.txt b/5_fluid/CMakeLists.txt
new file mode 100644
index 0000000..5a261ea
--- /dev/null
+++ b/5_fluid/CMakeLists.txt
@@ -0,0 +1,43 @@
+
+cmake_minimum_required(VERSION 3.1)
+project(5_fluid)
+
+set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/../cmake)
+set(CMAKE_CXX_FLAGS "-Wall")
+
+# libigl
+option(LIBIGL_USE_STATIC_LIBRARY "Use libigl as static library" OFF)
+option(LIBIGL_WITH_ANTTWEAKBAR       "Use AntTweakBar"    OFF)
+option(LIBIGL_WITH_CGAL              "Use CGAL"           OFF)
+option(LIBIGL_WITH_COMISO            "Use CoMiso"         OFF)
+option(LIBIGL_WITH_CORK              "Use Cork"           OFF)
+option(LIBIGL_WITH_EMBREE            "Use Embree"         OFF)
+option(LIBIGL_WITH_LIM               "Use LIM"            OFF)
+option(LIBIGL_WITH_MATLAB            "Use Matlab"         OFF)
+option(LIBIGL_WITH_MOSEK             "Use MOSEK"          OFF)
+option(LIBIGL_WITH_OPENGL            "Use OpenGL"         ON)
+option(LIBIGL_WITH_OPENGL_GLFW       "Use GLFW"           ON)
+option(LIBIGL_WITH_OPENGL_GLFW_IMGUI "Use ImGui"          ON)
+option(LIBIGL_WITH_PNG               "Use PNG"            OFF)
+option(LIBIGL_WITH_PYTHON            "Use Python"         OFF)
+option(LIBIGL_WITH_TETGEN            "Use Tetgen"         OFF)
+option(LIBIGL_WITH_TRIANGLE          "Use Triangle"       OFF)
+option(LIBIGL_WITH_VIEWER            "Use OpenGL viewer"  ON)
+option(LIBIGL_WITH_XML               "Use XML"            OFF)
+
+if (NOT LIBIGL_FOUND)
+    find_package(LIBIGL REQUIRED QUIET)
+endif()
+
+# Add default project files
+file(GLOB LIBFILES ${PROJECT_SOURCE_DIR}/../include/*.*)
+source_group("Library Files" FILES ${LIBFILES})
+include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../include)
+
+# Add your project files
+file(GLOB SRCFILES *.cpp)
+file(GLOB HFILES *.h)
+
+add_definitions(-DIGL_VIEWER_VIEWER_QUIET)
+add_executable(${PROJECT_NAME} ${SRCFILES} ${HFILES} ${LIBFILES} )
+target_link_libraries(${PROJECT_NAME} igl::core igl::opengl_glfw igl::opengl_glfw_imgui)
\ No newline at end of file
diff --git a/5_fluid/FluidSim.cpp b/5_fluid/FluidSim.cpp
new file mode 100644
index 0000000..c535541
--- /dev/null
+++ b/5_fluid/FluidSim.cpp
@@ -0,0 +1,188 @@
+#include "FluidSim.h"
+
+void FluidSim::solvePoisson() {
+	double dx2 = m_dx * m_dx;
+	double residual = m_acc + 1; // initial residual
+	double rho = 1;
+
+	Array2d& p = p_pressure->x();
+
+	for (int it = 0; residual > m_acc && it < m_iter; ++it) {
+		// Note that the boundaries are handles by the framework, so you iterations should be similar to:
+		for (int y = 1; y < m_res_y - 1; ++y) {
+			for (int x = 1; x < m_res_x - 1; ++x) {
+				double b = -p_divergence->x()(x, y) / m_dt * rho; // right-hand
+				// TODO: update the pressure values
+				p(x, y) = p(x, y);
+			}
+		}
+
+		// Compute the new residual, i.e. the sum of the squares of the individual residuals (squared L2-norm)
+		residual = 0;
+		for (int y = 1; y < m_res_y - 1; ++y) {
+			for (int x = 1; x < m_res_x - 1; ++x) {
+				double b = -p_divergence->x()(x, y) / m_dt * rho; // right-hand
+				// TODO: compute the cell residual
+				double cellResidual = 0;
+
+				residual += cellResidual * cellResidual;
+			}
+		}
+
+		// Get the L2-norm of the residual
+		residual = sqrt(residual);
+
+		// We assume the accuracy is meant for the average L2-norm per grid cell
+		residual /= (m_res_x - 2) * (m_res_y - 2);
+
+		// For your debugging, and ours, please add these prints after every iteration
+		if (it == m_iter - 1)
+			printf("Pressure solver: it=%d , res=%f \n", it, residual);
+		if (residual < m_acc)
+			printf("Pressure solver: it=%d , res=%f, converged \n", it, residual);
+	}
+}
+
+void FluidSim::correctVelocity() {
+	Array2d& p = p_pressure->x();
+	Array2d& u = p_velocity->x();	// x velocity
+	Array2d& v = p_velocity->y();	// y velocity
+
+	// Note: velocity u_{i+1/2}
+	for (int y = 1; y < m_res_y - 1; ++y)
+		for (int x = 1; x < m_res_x; ++x)
+			// TODO: update u
+			u(x, y) = u(x, y);
+
+	// Same for velocity v_{i+1/2}.
+	for (int y = 1; y < m_res_y; ++y)
+		for (int x = 1; x < m_res_x - 1; ++x)
+			// TODO: update v
+			v(x, y) = v(x, y);
+}
+
+void FluidSim::advectValues() {
+	// Densities live on the grid centers, the velocities on the MAC grid
+	// Separate their computation to avoid confusion
+
+	Array2d& d = p_density->x();
+	Array2d& u = p_velocity->x();
+	Array2d& v = p_velocity->y();
+
+	Array2d& d_ = p_density_tmp->x();
+	Array2d& u_ = p_velocity_tmp->x();
+	Array2d& v_ = p_velocity_tmp->y();
+
+	// Densities, grid centers
+	for (int y = 1; y < m_res_y - 1; ++y) {
+		for (int x = 1; x < m_res_x - 1; ++x) {
+			// TODO: Compute the velocity
+			double last_x_velocity = 0;
+			double last_y_velocity = 0;
+
+			// TODO: Find the last position of the particle (in grid coordinates)
+			double last_x = 0;
+			double last_y = 0;
+
+			// Make sure the coordinates are inside the boundaries
+			// Densities are known between 1 and res-2
+			if (last_x < 1) last_x = 1;
+			if (last_y < 1) last_y = 1;
+			if (last_x > m_res_x - 2) last_x = m_res_x - 2;
+			if (last_y > m_res_y - 2) last_y = m_res_y - 2;
+
+			// Determine corners for bilinear interpolation
+			int x_low = (int)last_x;
+			int y_low = (int)last_y;
+			int x_high = x_low + 1;
+			int y_high = y_low + 1;
+
+			// Compute the interpolation weights
+			double x_weight = last_x - x_low;
+			double y_weight = last_y - y_low;
+
+			// TODO: Bilinear interpolation
+			d_(x, y) = d(x, y);
+		}
+	}
+
+	// Velocities (u), MAC grid
+	for (int y = 1; y < m_res_y - 1; ++y) {
+		for (int x = 1; x < m_res_x; ++x) {
+			// TODO: Compute the velocity
+			double last_x_velocity = 0;
+			double last_y_velocity = 0;
+
+			// TODO: Find the last position of the particle (in grid coordinates)
+			double last_x = 0;
+			double last_y = 0;
+
+			// Make sure the coordinates are inside the boundaries
+			// Being conservative, one can say that the velocities are known between 1.5 and res-2.5
+			// (the MAC grid is inside the known densities, which are between 1 and res - 2)
+			if (last_x < 1.5) last_x = 1.5;
+			if (last_y < 1.5) last_y = 1.5;
+			if (last_x > m_res_x - 1.5) last_x = m_res_x - 1.5;
+			if (last_y > m_res_y - 2.5) last_y = m_res_y - 2.5;
+
+			// Determine corners for bilinear interpolation
+			int x_low = (int)last_x;
+			int y_low = (int)last_y;
+			int x_high = x_low + 1;
+			int y_high = y_low + 1;
+
+			// Compute the interpolation weights
+			double x_weight = last_x - x_low;
+			double y_weight = last_y - y_low;
+
+			// TODO: Bilinear interpolation
+			u_(x, y) = u(x, y);
+		}
+	}
+
+	// Velocities (v), MAC grid
+	for (int y = 1; y < m_res_y; ++y) {
+		for (int x = 1; x < m_res_x - 1; ++x) {
+			// TODO: Compute the velocity
+			double last_x_velocity = 0;
+			double last_y_velocity = 0;
+
+			// TODO: Find the last position of the particle (in grid coordinates)
+			double last_x = 0;
+			double last_y = 0;
+
+			// Make sure the coordinates are inside the boundaries
+			// Being conservative, one can say that the velocities are known between 1.5 and res-2.5
+			// (the MAC grid is inside the known densities, which are between 1 and res - 2)
+			if (last_x < 1.5) last_x = 1.5;
+			if (last_y < 1.5) last_y = 1.5;
+			if (last_x > m_res_x - 2.5) last_x = m_res_x - 2.5;
+			if (last_y > m_res_y - 1.5) last_y = m_res_y - 1.5;
+
+			// Determine corners for bilinear interpolation
+			double x_low = (int)last_x;
+			double y_low = (int)last_y;
+			double x_high = x_low + 1;
+			double y_high = y_low + 1;
+
+			// Compute the interpolation weights
+			double x_weight = last_x - x_low;
+			double y_weight = last_y - y_low;
+
+			// TODO: Bilinear interpolation
+			v_(x, y) = v(x, y);
+		}
+	}
+
+	// Copy the values in temp to the original buffers
+	for (int y = 1; y < m_res_y - 1; ++y)
+		for (int x = 1; x < m_res_x - 1; ++x)
+			d(x, y) = d_(x, y);
+	for (int y = 1; y < m_res_y - 1; ++y)
+		for (int x = 1; x < m_res_x; ++x)
+			u(x, y) = u_(x, y);
+	for (int y = 1; y < m_res_y; ++y)
+		for (int x = 1; x < m_res_x - 1; ++x)
+			v(x, y) = v_(x, y);
+}
+
diff --git a/5_fluid/FluidSim.h b/5_fluid/FluidSim.h
new file mode 100644
index 0000000..24d2387
--- /dev/null
+++ b/5_fluid/FluidSim.h
@@ -0,0 +1,296 @@
+#include <igl/edges.h>
+#include "Simulation.h"
+#include "Grid2.h"
+#include "MACGrid2.h"
+
+using namespace std;
+
+/*
+ * Simulation of a simple smoke plume rising.
+ */
+class FluidSim : public Simulation {
+public:
+	FluidSim() : Simulation() { init(); }
+
+	virtual void init() override {
+		m_res_x = 128;
+		m_res_y = int(m_res_x*1.5); // 3:2 ratio
+		m_size_x = 1.0; //m_res_x; // or just 1.0
+		m_dx = m_size_x / m_res_x; // ! dx == dy
+		m_idx = m_res_x / m_size_x;
+		m_size_y = m_dx * m_res_y;
+		m_dt = 0.005 * sqrt((m_res_x + m_res_y) * 0.5);
+		m_acc = 1e-5;
+		m_iter = 2000;
+		m_field = 0;
+		m_velocityOn = false;
+		m_vScale = 20;
+		m_windOn = false;
+
+		p_density = new Grid2(m_res_x, m_res_y, m_dx);
+		p_density_tmp = new Grid2(m_res_x, m_res_y, m_dx);
+		p_pressure = new Grid2(m_res_x, m_res_y, m_dx);
+		p_divergence = new Grid2(m_res_x, m_res_y, m_dx);
+		p_vorticity = new Grid2(m_res_x, m_res_y, m_dx);
+		p_density->getMesh(m_renderV, m_renderF); // need to call once
+		
+		p_velocity = new MACGrid2(m_res_x, m_res_y, m_dx);
+		p_velocity_tmp = new MACGrid2(m_res_x, m_res_y, m_dx);
+		p_force = new MACGrid2(m_res_x, m_res_y, m_dx);
+		reset();
+	}
+
+	virtual void resetMembers() override {
+		p_density->reset();
+		p_density->applySource(0.45, 0.55, 0.1, 0.15);
+		p_pressure->reset();
+		p_divergence->reset();
+		p_velocity->reset();
+		p_force->reset();
+	}
+
+	virtual void updateRenderGeometry() override {
+		if (m_field == 0) {
+			p_density->getColors(m_renderC);
+		}
+		else if (m_field == 1) {
+			p_pressure->getColors(m_renderC, true);
+		}
+		else if (m_field == 2) {
+			p_divergence->getColors(m_renderC, true);
+		}
+		else if (m_field == 3) {
+			p_vorticity->getColors(m_renderC, true);
+		}
+		
+		if (m_velocityOn) {
+			p_velocity->updateEdges(m_vScale);
+		}
+	}
+
+	virtual bool advance() override {
+		// apply source in density field
+		p_density->applySource(0.45, 0.55, 0.1, 0.15);
+
+		// add in new forces
+		addBuoyancy();
+		if (m_windOn)
+			addWind();
+		addForce();
+
+		// remove divergence
+		solvePressure();
+
+		// advect everything
+		advectValues();
+
+		// reset forces
+		p_force->reset();
+
+		// advance m_time
+		m_time += m_dt;
+		m_step++;
+
+		return false;
+	}
+
+	virtual void renderRenderGeometry(
+		igl::opengl::glfw::Viewer& viewer) override {
+		viewer.data().set_mesh(m_renderV, m_renderF);
+		viewer.data().set_colors(m_renderC);
+
+		if (m_velocityOn) {
+			viewer.data().add_edges(p_velocity->s(), p_velocity->e(), Eigen::RowVector3d(0, 0, 0));
+			viewer.data().add_edges(p_velocity->vs(), p_velocity->ve(), p_velocity->vc());
+		}
+	}	
+#pragma region FluidSteps
+	void addBuoyancy() {
+		double scaling = 64.0 / m_res_x;
+
+		// add buoyancy
+		for (int i = 0; i < p_force->y().size(0); ++i) {
+			for (int j = 1; j < p_force->y().size(1) - 1; ++j) {
+				p_force->y()(i, j) += 0.1 * (p_density->x()(i, j - 1) + p_density->x()(i, j)) / 2.0 * scaling;
+			}
+		}
+	}
+
+	void addWind() {
+		double scaling = 64.0 / m_res_x;
+
+		static double r = 0.0;
+		r += 1;
+
+		const double fx = 2e-2 * cos(5e-2 * r) * cos(3e-2 * r) * scaling;
+
+		// add wind
+		for (int i = 0; i < p_force->x().size(0); ++i) {
+			for (int j = 0; j < p_force->x().size(1); ++j) {
+				p_force->x()(i, j) += fx;
+			}
+		}
+	}
+
+	void addForce() {
+		for (int i = 0; i < p_velocity->x().size(0); ++i) {
+			for (int j = 0; j < p_velocity->x().size(1); ++j) {
+				p_velocity->x()(i, j) += m_dt * p_force->x()(i, j);
+			}
+		}
+
+		for (int i = 0; i < p_velocity->y().size(0); ++i) {
+			for (int j = 0; j < p_velocity->y().size(1); ++j) {
+				p_velocity->y()(i, j) += m_dt * p_force->y()(i, j);
+			}
+		}
+	}
+	
+	void solvePressure() {
+		// copy out the boundaries 
+		setNeumann();
+		setZero();
+
+		computeDivergence();
+
+		// solve Poisson equation
+		copyBorder();
+		solvePoisson();
+
+		correctVelocity();
+
+		computeVorticity();
+
+		// for debugging
+		computeDivergence();
+	}
+
+	void setNeumann() {
+		// x-velocity
+		Array2d& u = p_velocity->x();
+		int sx = u.size(0);
+		int sy = u.size(1);
+		for (int y = 0; y < sy; ++y) {
+			u(0, y) = u(2, y);
+			u(sx - 1, y) = u(sx - 3, y);
+		}
+
+		// y-velocity
+		Array2d& v = p_velocity->y();
+		sx = v.size(0);
+		sy = v.size(1);
+		for (int x = 0; x < sx; ++x) {
+			v(x, 0) = v(x, 2);
+			v(x, sy - 1) = v(x, sy - 3);
+		}
+	}
+
+	void setZero() {
+		// x-velocity
+		Array2d& u = p_velocity->x();
+		int sx = u.size(0);
+		int sy = u.size(1);
+		for (int x = 0; x < sx; ++x) {
+			u(x, 0) = 0;
+			u(x, sy - 1) = 0;
+		}
+
+		// y-velocity
+		Array2d& v = p_velocity->y();
+		sx = v.size(0);
+		sy = v.size(1);
+		for (int y = 0; y < sy; ++y) {
+			v(0, y) = 0;
+			v(sx - 1, y) = 0;
+		}
+	}
+
+	void computeDivergence() {
+		// calculate divergence
+		for (int y = 1; y < m_res_y - 1; ++y) {
+			for (int x = 1; x < m_res_x - 1; ++x) {
+				double xComponent = (p_velocity->x()(x + 1, y) - p_velocity->x()(x, y)) * m_idx;
+				double yComponent = (p_velocity->y()(x, y + 1) - p_velocity->y()(x, y)) * m_idx;
+				p_divergence->x()(x, y) = xComponent + yComponent;
+			}
+		}
+	}
+
+	void computeVorticity() {
+		// calculate vorticity
+		for (int y = 1; y < m_res_y - 1; ++y) {
+			for (int x = 1; x < m_res_x - 1; ++x) {
+				double xComponent = (p_velocity->x()(x + 1, y) - p_velocity->x()(x, y)) * m_idx;
+				double yComponent = (p_velocity->y()(x, y + 1) - p_velocity->y()(x, y)) * m_idx;
+				p_vorticity->x()(x, y) = yComponent - xComponent;
+			}
+		}
+	}
+
+	void copyBorder() {
+		Array2d& p = p_pressure->x();
+		int sx = p.size(0);
+		int sy = p.size(1);
+		for (int y = 0; y < sy; ++y) {
+			p(0, y) = p(1, y);
+			p(sx - 1, y) = p(sx - 2, y);
+		}
+		for (int x = 0; x < sx; ++x) {
+			p(x, 0) = p(x, 1);
+			p(x, sy - 1) = p(x, sy - 2);
+		}
+	}
+
+#pragma endregion FluidSteps
+
+#pragma region Exercise
+	void solvePoisson();
+	void correctVelocity();
+	void advectValues();
+#pragma endregion Exercise
+
+#pragma region SettersAndGetters
+	void selectField(int field) { m_field = field; }
+	void selectVField(bool v) { m_velocityOn = v; }
+	void setVelocityScale(double s) { m_vScale = s; }
+	void setResX(int r) { m_res_x = r; }
+	void setResY(int r) { m_res_y = r; }
+	void setAccuracy(double acc) { m_acc = acc; }
+	void setIteration(int iter) { m_iter = iter; }
+	void setWind(bool w) { m_windOn = w; }
+
+	int getField() const { return m_field; }
+	bool getVField() const { return m_velocityOn; }
+	double getVelocityScale() const { return m_vScale; }
+	int getResX() const { return m_res_x; }
+	int getResY() const { return m_res_y; }
+	double getAccuracy() const { return m_acc; }
+	int getIteration() const { return m_iter; }
+	bool getWind() const { return m_windOn; }
+	double getTimestep() const { return m_dt; }
+#pragma endregion SettersAndGetters
+
+private:
+	int m_res_x, m_res_y;
+	double m_dx, m_idx;			// dx, inverse dx
+	double m_size_x, m_size_y;
+	double m_acc;				// solver accuracy
+	int m_iter;
+	int m_field;
+	bool m_velocityOn;
+	double m_vScale;
+	bool m_windOn;
+
+	Grid2* p_density;
+	Grid2* p_density_tmp;
+	Grid2* p_pressure;
+	Grid2* p_divergence;
+	Grid2* p_vorticity;
+	MACGrid2* p_velocity;
+	MACGrid2* p_velocity_tmp;
+	MACGrid2* p_force;
+
+	Eigen::MatrixXd m_renderV; // vertex positions, 
+	Eigen::MatrixXi m_renderF; // face indices 
+	Eigen::MatrixXd m_renderC; // face (or vertex) colors for rendering
+};
\ No newline at end of file
diff --git a/5_fluid/main.cpp b/5_fluid/main.cpp
new file mode 100644
index 0000000..087e23d
--- /dev/null
+++ b/5_fluid/main.cpp
@@ -0,0 +1,78 @@
+#include <igl/writeOFF.h>
+#include <thread>
+#include "Gui.h"
+#include "Simulator.h"
+#include "FluidSim.h"
+
+/*
+ */
+class FluidGui : public Gui {
+public:
+	float m_dt;
+	float m_acc;
+	int m_iter;
+	float m_vScale;
+	bool m_windOn;
+
+	const vector<char const*> m_fields = {
+	   "Density", "Pressure", "Divergence", "Vorticity"
+	};
+	int m_selected_field;
+	bool m_velocityOn;
+
+	FluidSim* p_fluidSim = NULL;
+
+	FluidGui() {
+		turnOffLight(); // no light for field visualization
+
+		p_fluidSim = new FluidSim();
+		m_dt = p_fluidSim->getTimestep();
+		m_acc = p_fluidSim->getAccuracy();
+		m_iter = p_fluidSim->getIteration();
+		m_vScale = p_fluidSim->getVelocityScale();
+		m_windOn = p_fluidSim->getWind();
+		m_selected_field = p_fluidSim->getField();
+		m_velocityOn = p_fluidSim->getVField();
+		setSimulation(p_fluidSim);
+		start();
+	}
+
+	virtual void updateSimulationParameters() override {
+		// change all parameters of the simulation to the values that are set in
+		// the GUI
+		p_fluidSim->setVelocityScale(m_vScale);
+		p_fluidSim->setTimestep(m_dt);
+		p_fluidSim->setAccuracy(m_acc);
+		p_fluidSim->setIteration(m_iter);
+	}
+
+	virtual void clearSimulation() override {
+		p_fluidSim->setWind(m_windOn);
+	}
+
+	virtual void drawSimulationParameterMenu() override {
+		if (ImGui::Combo("Fields", &m_selected_field,
+			m_fields.data(), m_fields.size())) {
+			cout << m_selected_field << endl;
+			p_fluidSim->selectField(m_selected_field);
+			p_fluidSim->updateRenderGeometry();
+		}
+		if (ImGui::Checkbox("Velocity", &m_velocityOn)) {
+			p_fluidSim->selectVField(m_velocityOn);
+			p_fluidSim->updateRenderGeometry();
+		}
+		ImGui::InputFloat("v scale", &m_vScale, 0, 0);
+		ImGui::InputFloat("dt", &m_dt, 0, 0);
+		ImGui::InputFloat("accuracy", &m_acc, 0, 0, 5);
+		ImGui::InputInt("iter", &m_iter, 0, 0);
+		if (ImGui::Checkbox("Wind", &m_windOn))
+			p_fluidSim->setWind(m_windOn);
+	}
+};
+
+int main(int argc, char* argv[]) {
+	// create a new instance of the GUI for the spring simulation
+	new FluidGui();
+
+	return 0;
+}
\ No newline at end of file
diff --git a/CMakeLists.txt b/CMakeLists.txt
index fa70829..1743953 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -34,5 +34,6 @@ endif()
 # add_subdirectory(2-1_bead)
 # add_subdirectory(2-2_pendulum)
 # add_subdirectory(3_cloth)
-add_subdirectory(4-1_spinning)
-add_subdirectory(4-2_collision)
\ No newline at end of file
+# add_subdirectory(4-1_spinning)
+# add_subdirectory(4-2_collision)
+add_subdirectory(5_fluid)
\ No newline at end of file
-- 
GitLab