第一次接触SPH流体模拟时,我被那些流动的粒子深深吸引。SPH(光滑粒子流体动力学)就像是用无数个小水珠来模拟整个水流,每个粒子都像是一个微型传感器,记录着位置、速度和密度。这种无网格方法特别适合模拟自由表面流动,比如瀑布、海浪或者熔岩喷发。
在传统网格方法中,你需要为整个流体区域划分网格,而SPH完全跳出了这个框架。想象一下用一堆弹珠来模拟水流——弹珠之间通过某种"默契"(核函数)相互影响,距离越近影响越大。这种特性让SPH在处理大变形和复杂边界时游刃有余。
但在WebGL环境下,事情变得棘手。浏览器不是游戏引擎,我们需要巧妙利用GPU纹理来存储粒子数据。我尝试过直接把粒子数据存在JavaScript数组中,结果帧率直接跌到个位数。后来发现,把粒子状态编码到纹理中才是正道,就像把Excel表格转成图片格式,让GPU能快速读取。
把SPH塞进Cesium可不是简单的拼积木。Cesium本身是个地理可视化引擎,不是物理模拟器。我的第一个坑就是坐标系转换——Cesium用WGS84椭球体,而SPH计算通常在局部直角坐标系中进行。解决方案是在两者之间建立桥梁:SPH计算用局部坐标,渲染时再转换到地理坐标。
粒子存储结构设计是另一个关键点。我参考了WebGL的纹理数组概念,把粒子数据分层存储。比如,用RGBA通道分别存储位置(x,y,z)和密度,这样一张2048x2048的纹理就能处理百万级粒子。下面是核心数据结构示例:
glsl复制// 粒子数据结构定义
struct Particle {
vec3 position;
vec3 velocity;
float density;
float pressure;
};
实时交互是最让人兴奋的部分。通过Cesium的相机事件系统,我实现了用鼠标"搅动"流体的功能。当用户点击屏幕时,代码会计算点击位置对应的三维坐标,并在该区域施加力场。看着流体随着鼠标移动而翻涌,那种成就感简直无法形容。
WebGL的并行计算能力是SPH流畅运行的关键。我的方案是把计算分成两个阶段:先在Fragment Shader中更新粒子状态,再用Vertex Shader进行渲染。这种"双通道"设计避免了CPU-GPU之间的数据往返。
核函数的选择直接影响模拟效果。经过多次测试,我最终采用了三次样条核函数:
glsl复制float W(vec3 r, float h) {
float q = length(r) / h;
if (q > 2.0) return 0.0;
if (q > 1.0) return 0.25 * pow(2.0 - q, 3.0);
return 0.25 * (4.0 - 6.0*q*q + 3.0*q*q*q);
}
压力计算是最耗性能的部分。我优化了邻居搜索算法,利用空间哈希将时间复杂度从O(n²)降到O(n)。具体做法是把空间划分成均匀网格,每个粒子只需要检查相邻27个网格中的粒子。
原始粒子数据看起来就像一堆散乱的像素点。要让它们看起来像真正的流体,需要些视觉技巧。我开发了基于屏幕空间的光照模型,通过计算深度差异来模拟流体表面。
着色器中的关键步骤包括:
glsl复制// 屏幕空间法线计算
vec3 computeNormal(vec2 uv) {
vec2 delta = 1.0 / resolution;
float hL = texture2D(positionTexture, uv - vec2(delta.x, 0)).z;
float hR = texture2D(positionTexture, uv + vec2(delta.x, 0)).z;
float hD = texture2D(positionTexture, uv - vec2(0, delta.y)).z;
float hU = texture2D(positionTexture, uv + vec2(0, delta.y)).z;
return normalize(vec3(hL - hR, hD - hU, 2.0));
}
颜色处理也有讲究。我根据密度值动态调整流体颜色,高密度区域呈现深蓝色,低密度区域则接近透明。再加上适当的雾效和反射,最终效果比单纯的点精灵渲染真实得多。
让流体与Cesium地形互动是最具挑战性的部分。我的解决方案是生成地形的高度图纹理,在着色器中实时检测碰撞。当粒子高度低于地形高度时,施加一个向上的反弹力。
交互力的计算需要考虑多个因素:
glsl复制float terrainHeight = texture2D(heightMap, uv).r;
if (particlePos.z < terrainHeight) {
vec3 normal = computeTerrainNormal(uv);
vec3 reflectDir = reflect(velocity, normal);
velocity = reflectDir * restitution;
}
对于3D模型障碍物,我采用了SDF(有符号距离场)近似。预先计算模型SDF并上传到纹理,着色器中通过三次线性采样判断碰撞。虽然精度不如精确几何检测,但在视觉效果上已经足够好。
在低端设备上跑百万级粒子?听起来像天方夜谭,但通过以下技巧我做到了:
粒子LOD系统:
计算分帧策略:
纹理压缩技巧:
调试这类项目需要特殊工具。我强烈推荐使用WebGL Inspector插件,它可以让你暂停着色器执行,逐帧检查纹理状态。有次我就是靠它发现了一个诡异的边界条件错误——某些粒子会莫名其妙地飞出场景。
经过三个月的迭代,我的SPH实现已经可以稳定运行在主流设备上。下面是关键参数表:
| 参数 | 初始值 | 优化值 | 效果提升 |
|---|---|---|---|
| 粒子数 | 10k | 500k | 50倍 |
| 帧率 | 15fps | 60fps | 4倍 |
| 内存占用 | 500MB | 80MB | 84%↓ |
最终的流体效果可以模拟多种现象:
与商业引擎相比,这个方案的优势在于:
最让我自豪的是实现了多流体交互——不同密度的流体会自然分层,就像油和水的关系。这通过在粒子数据结构中添加type字段实现,每种类型有独立的物理参数。