写一个简单的流体仿真

导入

实现一个简单的SPH流体仿真效果

  • SPH算法

  • Tait方程

  • 空间哈希网格优化

  • openMP并行计算密度,压力,合力以及积分

完整代码

直接给出完整代码,写在一个文件内,方便读者使用和查看,就不多说数学原理方面的东西了,我也不喜欢说这些,SPH原理也并不复杂,代码中也写了详细注释:

//main.cpp
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

#include <vector>
#include <cmath>
#include <iostream>
#include <random>
#include <algorithm>
#include <omp.h> // OpenMP

// ==========================================
// 配置参数 (Configuration)
// ==========================================
const unsigned int SCR_WIDTH = 1280;
const unsigned int SCR_HEIGHT = 720;

// ==========================================
// 流体参数 (Tait Equation Setup)
// ==========================================

const int PARTICLE_COUNT = 4000;
const float SMOOTHING_RADIUS = 0.2f;
const float MASS = 0.1f;                 // 质量
const float REST_DENSITY = 120.0f;     

// --- Tait方程参数 ---
const float TAIT_EXPONENT = 7.0f;        // 水的典型指数,决定了"硬度"的爆发力
const float STIFFNESS = 800.0f;          // 基础刚度 (B)

// --- 动态参数 ---
const float VISCOSITY = 0.5f;            // 粘度
const float TIME_STEP = 0.0025f;         // 步长
const int SUB_STEPS = 8;                 // 子步数

const glm::vec3 GRAVITY(0.0f, -9.8f, 0.0f);
const float PARTICLE_RADIUS = 0.04f;

// --- 边界反弹 ---
const float BOUND_DAMPING = -0.8f;       // 保留80%动能,制造飞溅
const float WALL_FRICTION = 0.95f;       // 墙壁摩擦

// 容器边界
const glm::vec3 CONTAINER_SIZE(4.0f, 6.0f, 4.0f); // 宽, 高, 深 (以原点为中心)

constexpr float PI_F = 3.14159265358979323846f;
// ==========================================
//  辅助类: 摄像机 (Camera)
// ==========================================
class Camera {
public:
    glm::vec3 Position;
    glm::vec3 Front;
    glm::vec3 Up;
    glm::vec3 Right;
    glm::vec3 WorldUp;
    float Yaw = -90.0f;
    float Pitch = 0.0f;
    float MovementSpeed = 2.5f;
    float MouseSensitivity = 0.1f;

    Camera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 8.0f)) 
        : Position(position), Front(0.0f, 0.0f, -1.0f), WorldUp(0.0f, 1.0f, 0.0f) {
        updateCameraVectors();
    }

    glm::mat4 GetViewMatrix() const {
        return glm::lookAt(Position, Position + Front, Up);
    }

    void ProcessKeyboard(int direction, float deltaTime) {
        float velocity = MovementSpeed * deltaTime;
        if (direction == GLFW_KEY_W) Position += Front * velocity;
        if (direction == GLFW_KEY_S) Position -= Front * velocity;
        if (direction == GLFW_KEY_A) Position -= Right * velocity;
        if (direction == GLFW_KEY_D) Position += Right * velocity;
    }

    void ProcessMouseMovement(float xoffset, float yoffset) {
        xoffset *= MouseSensitivity;
        yoffset *= MouseSensitivity;
        Yaw += xoffset;
        Pitch += yoffset;
        if (Pitch > 89.0f) Pitch = 89.0f;
        if (Pitch < -89.0f) Pitch = -89.0f;
        updateCameraVectors();
    }

private:
    void updateCameraVectors() {
        glm::vec3 front;
        front.x = cos(glm::radians(Yaw)) * cos(glm::radians(Pitch));
        front.y = sin(glm::radians(Pitch));
        front.z = sin(glm::radians(Yaw)) * cos(glm::radians(Pitch));
        Front = glm::normalize(front);
        Right = glm::normalize(glm::cross(Front, WorldUp));
        Up = glm::normalize(glm::cross(Right, Front));
    }
};

// ==========================================
// SPH 求解器 (Physics System)
// ==========================================
class SPHSystem {
public:
    // 使用 SoA (Structure of Arrays) 布局优化缓存
    std::vector<glm::vec3> positions;
    std::vector<glm::vec3> velocities;
    std::vector<glm::vec3> forces;
    std::vector<float> densities;
    std::vector<float> pressures;

    // 空间哈希网格优化
    // key: 网格索引 (x + y*width + z*width*depth), value: 粒子索引列表
    std::vector<std::vector<int>> grid; 
    int gridResX, gridResY, gridResZ;
    glm::vec3 gridMin, gridMax;
    float cellSize;

    SPHSystem() {
        positions.resize(PARTICLE_COUNT);
        velocities.resize(PARTICLE_COUNT);
        forces.resize(PARTICLE_COUNT);
        densities.resize(PARTICLE_COUNT);
        pressures.resize(PARTICLE_COUNT);

        // 初始化网格参数
        cellSize = SMOOTHING_RADIUS;
        gridMin = -CONTAINER_SIZE / 2.0f - glm::vec3(1.0f);
        gridMax = CONTAINER_SIZE / 2.0f + glm::vec3(1.0f);
        gridResX = (int)ceil((gridMax.x - gridMin.x) / cellSize);
        gridResY = (int)ceil((gridMax.y - gridMin.y) / cellSize);
        gridResZ = (int)ceil((gridMax.z - gridMin.z) / cellSize);
        grid.resize(gridResX * gridResY * gridResZ);

        reset();
    }

    void reset() {
        std::mt19937 rng(std::random_device{}());
        std::uniform_real_distribution<float> dist(-0.5f, 0.5f);

        // 将粒子生成在空中的一个立方体区域内
        float spawnSize = 2.0f;
        int particlesPerAxis = (int)ceil(cbrt(PARTICLE_COUNT));
        float spacing = spawnSize / particlesPerAxis;

        int idx = 0;
        for(int x=0; x<particlesPerAxis && idx < PARTICLE_COUNT; ++x) {
            for(int y=0; y<particlesPerAxis && idx < PARTICLE_COUNT; ++y) {
                for(int z=0; z<particlesPerAxis && idx < PARTICLE_COUNT; ++z) {
                    // 添加一些随机抖动避免由于完美对齐导致的数值不稳定
                    float jitterX = dist(rng) * 0.01f;
                    float jitterY = dist(rng) * 0.01f;
                    float jitterZ = dist(rng) * 0.01f;

                    // 初始位置在容器上方
                    positions[idx] = glm::vec3(
                        (x * spacing) - spawnSize/2.0f + jitterX,
                        (y * spacing) + 2.0f + jitterY, 
                        (z * spacing) - spawnSize/2.0f + jitterZ
                    );
                    // velocities[idx] = glm::vec3(0.0f);
                    // 在生成位置后,给一个随机微小初速度
                    velocities[idx] = glm::vec3(
                        dist(rng) * 2.0f, // X方向有点初速度
                        dist(rng) * 2.0f, 
                        dist(rng) * 2.0f
                    );
                    idx++;
                }
            }
        }
    }

    // SPH 核函数: Poly6 (用于密度计算)
    float poly6Kernel(float r2) {
        float h2 = SMOOTHING_RADIUS * SMOOTHING_RADIUS;
        if (r2 < 0 || r2 > h2) return 0.0f;
        float diff = h2 - r2;
        return (315.0f / (64.0f * PI_F * pow(SMOOTHING_RADIUS, 9))) * diff * diff * diff;
    }

    // SPH 核函数梯度: Spiky (用于压力计算)
    glm::vec3 spikyKernelGradient(glm::vec3 r, float dist) {
        if (dist <= 0 || dist > SMOOTHING_RADIUS) return glm::vec3(0.0f);
        float t = SMOOTHING_RADIUS - dist;
        float scalar = -45.0f / (PI_F * pow(SMOOTHING_RADIUS, 6)) * t * t;
        return glm::normalize(r) * scalar;
    }

    // SPH 核函数拉普拉斯: Viscosity (用于粘性计算)
    float viscosityKernelLaplacian(float dist) {
        if (dist <= 0 || dist > SMOOTHING_RADIUS) return 0.0f;
        return 45.0f / (PI_F * pow(SMOOTHING_RADIUS, 6)) * (SMOOTHING_RADIUS - dist);
    }

    // 计算网格索引
    int getGridIndex(const glm::vec3& pos) {
        int x = (int)((pos.x - gridMin.x) / cellSize);
        int y = (int)((pos.y - gridMin.y) / cellSize);
        int z = (int)((pos.z - gridMin.z) / cellSize);

        // 边界钳制,防止越界
        x = std::clamp(x, 0, gridResX - 1);
        y = std::clamp(y, 0, gridResY - 1);
        z = std::clamp(z, 0, gridResZ - 1);

        return x + y * gridResX + z * gridResX * gridResY;
    }

    void update() {
        // 1. 构建空间网格
        // 清空网格
        for(auto& cell : grid) cell.clear();

        // 将粒子填入网格
        for (int i = 0; i < PARTICLE_COUNT; ++i) {
            grid[getGridIndex(positions[i])].push_back(i);
        }

        // 2. 计算密度和压力
        #pragma omp parallel for schedule(static)
        for (int i = 0; i < PARTICLE_COUNT; ++i) {
            densities[i] = 0.0f;

            // 获取当前粒子所在及其周围的网格 (3x3x3)
            int cx = (int)((positions[i].x - gridMin.x) / cellSize);
            int cy = (int)((positions[i].y - gridMin.y) / cellSize);
            int cz = (int)((positions[i].z - gridMin.z) / cellSize);

            for (int dx = -1; dx <= 1; ++dx) {
                for (int dy = -1; dy <= 1; ++dy) {
                    for (int dz = -1; dz <= 1; ++dz) {
                        int nx = cx + dx;
                        int ny = cy + dy;
                        int nz = cz + dz;

                        if (nx >= 0 && nx < gridResX && ny >= 0 && ny < gridResY && nz >= 0 && nz < gridResZ) {
                            int cellIdx = nx + ny * gridResX + nz * gridResX * gridResY;
                            for (int j : grid[cellIdx]) {
                                glm::vec3 diff = positions[i] - positions[j];
                                float r2 = glm::dot(diff, diff);
                                if (r2 < SMOOTHING_RADIUS * SMOOTHING_RADIUS) {
                                    densities[i] += MASS * poly6Kernel(r2);
                                }
                            }
                        }
                    }
                }
            }
            // 状态方程截断负压
            // 如果密度低于静态密度 (REST_DENSITY),压力设为 0,而不是负数。
            densities[i] = std::max(densities[i], 0.001f); // 防止除零

            // Tait Equation
            // P = B * ((rho / rho0)^gamma - 1)
            // 这种方程在密度接近1时压力很小,但稍微压缩一点压力就暴增
            float densityRatio = densities[i] / REST_DENSITY;

            // 截断负压:只允许 densityRatio > 1 时产生正压
            if (densityRatio > 1.0f) {
                pressures[i] = STIFFNESS * (std::pow(densityRatio, TAIT_EXPONENT) - 1.0f);
            } else {
                pressures[i] = 0.0f;
            }

        }

        // 3. 计算合力 (压力 + 粘性 + 重力)
        #pragma omp parallel for schedule(static)
        for (int i = 0; i < PARTICLE_COUNT; ++i) {
            glm::vec3 pressureForce(0.0f);
            glm::vec3 viscosityForce(0.0f);

            int cx = (int)((positions[i].x - gridMin.x) / cellSize);
            int cy = (int)((positions[i].y - gridMin.y) / cellSize);
            int cz = (int)((positions[i].z - gridMin.z) / cellSize);

            for (int dx = -1; dx <= 1; ++dx) {
                for (int dy = -1; dy <= 1; ++dy) {
                    for (int dz = -1; dz <= 1; ++dz) {
                        int nx = cx + dx; 
                        int ny = cy + dy;
                        int nz = cz + dz;

                        if (nx >= 0 && nx < gridResX && ny >= 0 && ny < gridResY && nz >= 0 && nz < gridResZ) {
                            int cellIdx = nx + ny * gridResX + nz * gridResX * gridResY;
                            for (int j : grid[cellIdx]) {
                                if (i == j) continue;

                                glm::vec3 diff = positions[i] - positions[j];
                                float dist = glm::length(diff);

                                // 最小距离钳制
                                // 防止两个粒子重合时产生 NaN 或者 Inf 的力
                                if (dist < 0.0001f) dist = 0.0001f; 

                                if (dist < SMOOTHING_RADIUS && dist > 0.0001f) {
                                    // 压力 (对称)
                                    glm::vec3 grad = spikyKernelGradient(diff, dist);
                                    pressureForce -= MASS * (pressures[i] + pressures[j]) / (2.0f * densities[j]) * grad;

                                    // 粘滞力
                                    float lap = viscosityKernelLaplacian(dist);
                                    viscosityForce += VISCOSITY * MASS * (velocities[j] - velocities[i]) / densities[j] * lap;
                                }
                            }
                        }
                    }
                }
            }

            glm::vec3 gravityForce = GRAVITY * densities[i];
            forces[i] = pressureForce + viscosityForce + gravityForce;
        }

        // 4. 积分 (Integration) 与 边界碰撞
        #pragma omp parallel for schedule(static)
        for (int i = 0; i < PARTICLE_COUNT; ++i) {
            // F = ma => a = F/rho
            glm::vec3 acceleration = forces[i] / densities[i];

            velocities[i] += acceleration * TIME_STEP;

            // 放宽速度限制,只拦截极端错误的数值爆炸
            float vLength = glm::length(velocities[i]);
            if (vLength > 20.0f) {
                velocities[i] = glm::normalize(velocities[i]) * 20.0f;
            }

            positions[i] += velocities[i] * TIME_STEP;

            // 边界检测
            // 容器半长宽 (Box Half-Size)
            glm::vec3 bounds = CONTAINER_SIZE / 2.0f;
            // 边界检测需考虑粒子半径 (r),防止穿模
            float r = PARTICLE_RADIUS;
            bool collided = false;

            // X轴碰撞 增加切向摩擦力,让水流撞墙后产生翻滚效果
            if (positions[i].x < -bounds.x + r) {
                positions[i].x = -bounds.x + r;
                velocities[i].x *= BOUND_DAMPING;
                velocities[i].y *= WALL_FRICTION; // 切向摩擦
                velocities[i].z *= WALL_FRICTION;
            }
            else if (positions[i].x > bounds.x - r) {
                positions[i].x = bounds.x - r;
                velocities[i].x *= BOUND_DAMPING;
                velocities[i].y *= WALL_FRICTION;
                velocities[i].z *= WALL_FRICTION;
            }

            // Y轴碰撞 (地板)
            if (positions[i].y < -bounds.y + r) {
                positions[i].y = -bounds.y + r;
                velocities[i].y *= BOUND_DAMPING; // 强力反弹
                velocities[i].x *= WALL_FRICTION; // 地面摩擦
                velocities[i].z *= WALL_FRICTION;
            }

            // Z轴碰撞
            if (positions[i].z < -bounds.z + r) {
                positions[i].z = -bounds.z + r;
                velocities[i].z *= BOUND_DAMPING;
                velocities[i].x *= WALL_FRICTION;
                velocities[i].y *= WALL_FRICTION;
            }
            else if (positions[i].z > bounds.z - r) {
                positions[i].z = bounds.z - r;
                velocities[i].z *= BOUND_DAMPING;
                velocities[i].x *= WALL_FRICTION;
                velocities[i].y *= WALL_FRICTION;
            }
        }
    }
};

// ==========================================
// 渲染器 (Shader & Setup)
// ==========================================

// 顶点着色器: 支持实例化渲染 (Instancing)
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;      // 基础球体模型顶点
layout (location = 1) in vec3 aNormal;   // 法线
layout (location = 2) in vec3 aOffset;   // 实例偏移量 (每个粒子的位置)

out vec3 Normal;
out vec3 FragPos;

uniform mat4 view;
uniform mat4 projection;
uniform float radius;

void main()
{
    // 计算实际世界坐标:模型偏移 + 顶点缩放
    vec3 worldPos = aOffset + aPos * radius; 
    FragPos = worldPos;
    Normal = aNormal; // 简化,假设球体不旋转

    gl_Position = projection * view * vec4(worldPos, 1.0);
}
)";

// 片段着色器: 简单的蓝色水体效果
const char* fragmentShaderSource = R"(
#version 330 core
out vec4 FragColor;

in vec3 Normal;
in vec3 FragPos;

uniform vec3 viewPos;

void main()
{
    // 环境光
    float ambientStrength = 0.3;
    vec3 ambient = ambientStrength * vec3(0.2, 0.5, 1.0);

    // 漫反射
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(vec3(0.5, 1.0, 0.5)); // 假定一个平行光
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = diff * vec3(0.0, 0.6, 1.0);

    // 镜面光
    float specularStrength = 0.8;
    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 reflectDir = reflect(-lightDir, norm);  
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
    vec3 specular = specularStrength * vec3(1.0, 1.0, 1.0);  

    vec3 result = ambient + diffuse + specular;
    FragColor = vec4(result, 0.85); // 略微透明
}
)";

// 构建简单球体网格
void createSphere(std::vector<float>& vertices, std::vector<unsigned int>& indices, int sectors, int stacks) {
    float x, y, z, xy;  
    float nx, ny, nz, lengthInv = 1.0f;         

    float sectorStep = 2 * PI_F / sectors;
    float stackStep = PI_F / stacks;
    float sectorAngle, stackAngle;

    for(int i = 0; i <= stacks; ++i) {
        stackAngle = PI_F / 2 - i * stackStep;
        xy = cos(stackAngle);
        z = sin(stackAngle);

        for(int j = 0; j <= sectors; ++j) {
            sectorAngle = j * sectorStep;
            x = xy * cos(sectorAngle);
            y = xy * sin(sectorAngle);

            // 顶点
            vertices.push_back(x);
            vertices.push_back(z); // 注意: 这里的生成逻辑y朝上,我们交换y/z适应习惯或Shader
            vertices.push_back(y);

            // 法线 (球体中心在原点,法线就是位置)
            vertices.push_back(x);
            vertices.push_back(z);
            vertices.push_back(y);
        }
    }

    int k1, k2;
    for(int i = 0; i < stacks; ++i) {
        k1 = i * (sectors + 1);
        k2 = k1 + sectors + 1;
        for(int j = 0; j < sectors; ++j, ++k1, ++k2) {
            if(i != 0) {
                indices.push_back(k1);
                indices.push_back(k2);
                indices.push_back(k1 + 1);
            }
            if(i != (stacks - 1)) {
                indices.push_back(k1 + 1);
                indices.push_back(k2);
                indices.push_back(k2 + 1);
            }
        }
    }
}

// 全局变量用于回调
Camera camera(glm::vec3(0.0f, 2.0f, 10.0f));
float lastX = SCR_WIDTH / 2.0f;
float lastY = SCR_HEIGHT / 2.0f;
bool firstMouse = true;
bool resetSim = false;

void mouse_callback(GLFWwindow* window, double xposIn, double yposIn) {
    float xpos = static_cast<float>(xposIn);
    float ypos = static_cast<float>(yposIn);
    if (firstMouse) { lastX = xpos; lastY = ypos; firstMouse = false; }
    float xoffset = xpos - lastX;
    float yoffset = lastY - ypos; 
    lastX = xpos; lastY = ypos;
    camera.ProcessMouseMovement(xoffset, yoffset);
}

void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) {
    if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
    if (key == GLFW_KEY_R && action == GLFW_PRESS)
        resetSim = true;
}

// 编译 Shader 辅助函数
unsigned int compileShader(const char* vSource, const char* fSource) {
    unsigned int vertex, fragment;
    int success;
    char infoLog[512];

    vertex = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertex, 1, &vSource, NULL);
    glCompileShader(vertex);
    glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
    if(!success) { glGetShaderInfoLog(vertex, 512, NULL, infoLog); std::cout << "Vertex Shader Error: " << infoLog << std::endl; }

    fragment = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragment, 1, &fSource, NULL);
    glCompileShader(fragment);
    glGetShaderiv(fragment, GL_COMPILE_STATUS, &success);
    if(!success) { glGetShaderInfoLog(fragment, 512, NULL, infoLog); std::cout << "Fragment Shader Error: " << infoLog << std::endl; }

    unsigned int ID = glCreateProgram();
    glAttachShader(ID, vertex);
    glAttachShader(ID, fragment);
    glLinkProgram(ID);
    glGetProgramiv(ID, GL_LINK_STATUS, &success);
    if(!success) { glGetProgramInfoLog(ID, 512, NULL, infoLog); std::cout << "Link Error: " << infoLog << std::endl; }

    glDeleteShader(vertex);
    glDeleteShader(fragment);
    return ID;
}


int main() {
    // 初始化 GLFW
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "Fluid Simulation (SPH)", NULL, NULL);
    if (window == NULL) {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetCursorPosCallback(window, mouse_callback);
    glfwSetKeyCallback(window, key_callback);
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

    // 初始化 GLAD
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    glEnable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    // 编译 Shader
    unsigned int shaderProgram = compileShader(vertexShaderSource, fragmentShaderSource);

    // 创建球体模型数据
    std::vector<float> sphereVertices;
    std::vector<unsigned int> sphereIndices;
    createSphere(sphereVertices, sphereIndices, 12, 12); // 低多边形球体以提升性能

    // 配置 OpenGL Buffer
    unsigned int VAO, VBO, EBO, instanceVBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);
    glGenBuffers(1, &instanceVBO);

    glBindVertexArray(VAO);

    // 1. 球体模型 VBO
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sphereVertices.size() * sizeof(float), sphereVertices.data(), GL_STATIC_DRAW);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sphereIndices.size() * sizeof(unsigned int), sphereIndices.data(), GL_STATIC_DRAW);

    // 属性 0: 顶点位置
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    // 属性 1: 法线
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);

    // 2. 实例位置 VBO 
    glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
    // 初始分配大小,但数据稍后更新 (DYNAMIC_DRAW)
    glBufferData(GL_ARRAY_BUFFER, PARTICLE_COUNT * sizeof(glm::vec3), NULL, GL_DYNAMIC_DRAW);

    // 属性 2: 实例偏移 (layout 2)
    glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
    glEnableVertexAttribArray(2);
    // 告诉 OpenGL 这个属性是每实例更新一次,而不是每顶点
    glVertexAttribDivisor(2, 1); 

    glBindVertexArray(0);

    // 初始化仿真系统
    SPHSystem sph;

    // 容器线框数据 (用于可视化边界)
    float containerVertices[] = {
        // 底部矩形
        -CONTAINER_SIZE.x/2, -CONTAINER_SIZE.y/2, -CONTAINER_SIZE.z/2,
         CONTAINER_SIZE.x/2, -CONTAINER_SIZE.y/2, -CONTAINER_SIZE.z/2,
         CONTAINER_SIZE.x/2, -CONTAINER_SIZE.y/2,  CONTAINER_SIZE.z/2,
        -CONTAINER_SIZE.x/2, -CONTAINER_SIZE.y/2,  CONTAINER_SIZE.z/2,
        // 顶部矩形
        -CONTAINER_SIZE.x/2,  CONTAINER_SIZE.y/2, -CONTAINER_SIZE.z/2,
         CONTAINER_SIZE.x/2,  CONTAINER_SIZE.y/2, -CONTAINER_SIZE.z/2,
         CONTAINER_SIZE.x/2,  CONTAINER_SIZE.y/2,  CONTAINER_SIZE.z/2,
        -CONTAINER_SIZE.x/2,  CONTAINER_SIZE.y/2,  CONTAINER_SIZE.z/2,
    };
    unsigned int containerIndices[] = {
        0, 1, 1, 2, 2, 3, 3, 0, // 底
        4, 5, 5, 6, 6, 7, 7, 4, // 顶
        0, 4, 1, 5, 2, 6, 3, 7  // 柱
    };
    unsigned int cVAO, cVBO, cEBO;
    glGenVertexArrays(1, &cVAO);
    glGenBuffers(1, &cVBO);
    glGenBuffers(1, &cEBO);
    glBindVertexArray(cVAO);
    glBindBuffer(GL_ARRAY_BUFFER, cVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(containerVertices), containerVertices, GL_STATIC_DRAW);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, cEBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(containerIndices), containerIndices, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    // 时间统计
    float lastFrame = 0.0f;

    // 定义物理子步数
    const int SUB_STEPS = 4; 

    while (!glfwWindowShouldClose(window)) {
        // 计算时间差
        float currentFrame = static_cast<float>(glfwGetTime());
        float deltaTime = currentFrame - lastFrame;
        lastFrame = currentFrame;

        // 输入处理
        if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) camera.ProcessKeyboard(GLFW_KEY_W, deltaTime);
        if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) camera.ProcessKeyboard(GLFW_KEY_S, deltaTime);
        if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) camera.ProcessKeyboard(GLFW_KEY_A, deltaTime);
        if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) camera.ProcessKeyboard(GLFW_KEY_D, deltaTime);

        // 逻辑重置
        if (resetSim) {
            sph.reset();
            resetSim = false;
        }


        // 无论当前渲染帧率是多少,物理引擎都必须以精细的步伐前进
        // 这能彻底解决粒子穿墙和爆炸的问题
        for (int s = 0; s < SUB_STEPS; ++s) {
            sph.update();
        }

        // 渲染准备
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
        glm::mat4 view = camera.GetViewMatrix();

        glUseProgram(shaderProgram);
        glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
        glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "view"), 1, GL_FALSE, glm::value_ptr(view));
        glUniform3fv(glGetUniformLocation(shaderProgram, "viewPos"), 1, glm::value_ptr(camera.Position));
        glUniform1f(glGetUniformLocation(shaderProgram, "radius"), PARTICLE_RADIUS);

        // 更新实例数据 (将最新的粒子位置传给 GPU)
        glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
        glBufferSubData(GL_ARRAY_BUFFER, 0, PARTICLE_COUNT * sizeof(glm::vec3), sph.positions.data());

        // 绘制流体粒子 (Instanced)
        glBindVertexArray(VAO);
        glDrawElementsInstanced(GL_TRIANGLES, (unsigned int)sphereIndices.size(), GL_UNSIGNED_INT, 0, PARTICLE_COUNT);

        // 绘制容器边界 (简单的白色线框)
        glBindVertexArray(cVAO);
        glUniform1f(glGetUniformLocation(shaderProgram, "radius"), 1.0f); // 容器不需要radius缩放
        glDrawElements(GL_LINES, 24, GL_UNSIGNED_INT, 0);

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glfwTerminate();
    return 0;
}
  • WASD自由移动。鼠标移动改变视角

  • R重置流体位置,重新渲染

CMakeLists.txt:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)

project(FluidSim CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 忽略警告 C4819
add_compile_options(/wd4819)

# 依赖包
find_package(glad CONFIG REQUIRED)
find_package(glfw3 CONFIG REQUIRED)
find_package(glm CONFIG REQUIRED)
find_package(OpenMP REQUIRED)

add_executable(FluidSim main.cpp)

# 链接 OpenMP 库
if(OpenMP_CXX_FOUND)
    target_link_libraries(FluidSim PRIVATE OpenMP::OpenMP_CXX)
endif()

target_link_libraries(FluidSim PRIVATE glad::glad glfw glm::glm)

OpenMP大多数现代编译器都是直接支持的,不用单独下载,直接find即可。其他glad,glfw3,glm和openGL,都是建议直接使用vcpkg包管理器直接下载,方便省事,无需太多配置。

运行效果

使用Ninja构建Release版本:

cmake -GNinja -B build -DCMAKE_TOOLCHAIN_FILE="D:/vcpkg/vcpkg/scripts/buildsystems/vcpkg.cmake" -DCMAKE_BUILD_TYPE=Release

cmake --build build

稍微更真实一点的水体粒子渲染效果:

进一步

  • 采用简单的CPU并行,只用了4000个粒子,如果想要支持更多的,几万十万级别的粒子,需要使用GPU并行计算,可以使用cuda,但是对于这个本身就使用OpenGL的项目,OpenGL4.3版本提出的compute shader计算着色器也是一个方便的选择,写起来更简单一点,这就交给读者自行去实现了。

  • 想要从粒子渲染到真实水面的话,业界最标准常用的做法就是SSFR(屏幕空间流体渲染)技术