重磅开源!Cesium通过动态顶点计算生成逼真水系统,还有波浪,侵蚀...

3,691 阅读6分钟

大家好,我是日拱一卒的攻城师不浪,致力于技术与艺术的融合。这是2024年输出的第46/100篇文章。

前言

群里的小伙伴经常会问,博主有没有Cesium做的水效果,非常逼真的那种,而不是简单的一个面着色或者是一个水材质面

好的,那么今天就应广大网友的要求,一个动态顶点计算的水效果开源给大家!

好了,效果是不是还不错呢?废话不多说,我们直接上理论以及代码!

原理

在做一个场景效果之前,我们先不要急着下手写代码,应该先分析下这个场景,它的架构,它都会涉及到哪些功能,整体梳理一遍,防止后期返工,做无用功。

动态水效果的实现基于以下几个关键技术:

  • 地形数据:使用Cesium提供的地形数据,结合地形的海拔信息来模拟水面的高度变化。

  • 纹理映射:通过将预先设计的纹理图像映射到水面上,模拟波浪的视觉效果。

  • 着色器(Shader):使用GLSL着色器语言编写的代码,动态计算每个像素的颜色和光照效果,实现水面的动态变化。

  • 噪声函数:使用噪声函数生成随机的波纹效果,增加水面的真实感。

  • 光照和反射:模拟太阳光的照射和水面的反射,增强水面的立体感和动态效果。

计算水的渲染区域

这里定义了一个函数createSquareRectangle,它能够根据中心点的经纬度边长,计算出一个矩形区域的边界。这个边界用于定义水面效果的区域。

const createSquareRectangle = (centerLon, centerLat, sideLength) => {
  // 将边长转换为度
  const earthRadius = 6371000; // 地球平均半径,单位:米
  const angularDistance = (sideLength / earthRadius) * (180 / Math.PI);

  // 计算经度差
  const lonDiff = angularDistance / Math.cos((centerLat * Math.PI) / 180);

  // 计算矩形的边界
  const west = centerLon - lonDiff / 2;
  const east = centerLon + lonDiff / 2;
  const south = centerLat - angularDistance / 2;
  const north = centerLat + angularDistance / 2;

  // 返回[west, south, east, north]格式的数组
  return [west, south, east, north];
}

初始化

定义一个配置对象config,包含了地形的最小和最大海拔高度,以及用于水面纹理的图片URL和中心点坐标。

const config = {
  minElevation: 1153.0408311859962,
  maxElevation: 3158.762303474051,
  url: "/images/texture2.png",
  center: [-119.5509508318, 37.7379837881],
};

获取图像源

定义getImageSource数异步获取一个图像资源,并返回一个包含最小海拔、最大海拔和图像的对象。

const getImageSource = async () => {
  const image = await Cesium.Resource.fetchImage({
    url: config.url,
  });
  return {
    minElevation: config.minElevation,
    maxElevation: config.maxElevation,
    canvas: image,
  };
}

初始化地形

设置地形provider,我们直接使用Cesium的Ion服务,并请求顶点法线,这对于水面效果的渲染是必须的。

viewer.scene.terrainProvider = await Cesium.CesiumTerrainProvider.fromIonAssetId(1, {
  requestVertexNormals: true,
})

封装动态水

我们使用自定义的着色器和计算模型,渲染动态水,包括波浪海岸线的渐变效果、光照反射粒子效果等。

定义一个Erosion类,其中涉及了多个复杂的图形计算和水面模拟。

1.常量和统一变量

uniform sampler2D heightMap;
uniform float heightScale;
uniform float maxElevation;
uniform float minElevation;
uniform sampler2D iChannel0;
uniform float iTime;

uniform float coast2water_fadedepth;
uniform float large_waveheight;
uniform float large_wavesize;
uniform float small_waveheight;
uniform float small_wavesize;
uniform float water_softlight_fact;
uniform float water_glossylight_fact;
uniform float particle_amount;
uniform float WATER_LEVEL;

这些uniform变量主要用于水面渲染的动态调整,我们后边会通过dat.gui插件进行动态可视化控制:

  • heightMap:高度图,用于显示地形的海拔变化。

  • iChannel0:用于生成噪声的纹理。

  • iTime:当前时间,控制动画的进度。

  • coast2water_fadedepthlarge_waveheightsmall_waveheight等:用于控制水面的波浪效果、波浪大小、海岸线到水的渐变等。

2. 噪声函数和水面计算

float noise(vec2 p) {
    return textureLod(iChannel0, p * vec2(1. / 256.), 0.0).x;
}

float water_map(vec2 p, float height) {
    vec2 p2 = p * large_wavesize;
    vec2 shift1 = 0.001 * vec2(iTime * 160.0 * 2.0, iTime * 120.0 * 2.0);
    vec2 shift2 = 0.001 * vec2(iTime * 190.0 * 2.0, -iTime * 130.0 * 2.0);

    float f = 0.6000 * noise(p);
    f += 0.2500 * noise(p * m);
    f += 0.1666 * noise(p * m * m);
    float wave = sin(p2.x * 0.622 + p2.y * 0.622 + shift2.x * 4.269) * large_waveheight * f * height * height;

    p *= small_wavesize;
    f = 0.;
    float amp = 1.0, s = .5;
    for(int i = 0; i < 9; i++) {
        p = m * p * .947;
        f -= amp * abs(sin((noise(p + shift1 * s) - .5) * 2.));
        amp = amp * .59;
        s *= -1.329;
    }

    return wave + f * small_waveheight;
}
  • noise函数:通过纹理采样生成2D噪声值,用于模拟水面的波动。

  • water_map函数:结合大波浪和小波浪的噪声,模拟出水面的波浪效果。large_wavesizesmall_wavesize控制波浪的大小和形态。

3. 水面反射和光照

vec3 light;
float specular = pow(grad, water_softlight_fact);  // used for soft highlights
float specular2 = pow(grad, water_glossylight_fact); // used for glossy highlights
  • 计算光照的高光部分,specularspecular2分别用于软高光和光滑高光,模拟光线在水面上的反射。

4. 海岸线和水体过渡

float coastfade = clamp((level - height) / coast2water_fadedepth, 0., 1.);
  • coastfade:用于计算水面与海岸的过渡效果,coast2water_fadedepth控制过渡的深度。它用于渐变效果,使得水面逐渐变深。

5. 粒子效果

float particles(vec2 p) {
    p *= 200.;
    float f = 0.;
    float amp = 1.0, s = 1.5;
    for(int i = 0; i < 3; i++) {
        p = m * p * 1.2;
        f += amp * noise(p + iTime * s);
        amp = amp * .5;
        s *= -1.227;
    }
    return pow(f * .35, 7.) * particle_amount;
}
  • particles:通过噪声函数模拟粒子效果,用于在水面上生成动态的粒子系统。

6. 顶点着色器

vec3 worldToGeographic(vec3 worldPosition) {
    ...
    return vec3(lon, lat, alt);
}

vec3 deg2cartesian(vec3 deg) {
    ...
    return geo2cartesian(geo);
}
  • worldToGeographic:将世界坐标转换为经纬度和高度。

  • deg2cartesian:将经纬度和高度转换回笛卡尔坐标系,用于调整顶点的高度和位置。

7. 片段着色器

在片段着色器中,水体的颜色通过多个因素混合:

  • 水体颜色:使用watercolorwatercolor2water_specularcolor等控制水面的颜色和反射。

  • 渐变效果:根据海岸线到水的深度,混合不同的颜色来表示浅水和深水区域。

  • 光照和阴影:计算水面的高光、阴影等效果,模拟水面反射的真实感。

8. 水面和地形的结合

在着色器中,水面高度(level)会根据高度图和动态波浪变化进行调整。当地形的高度低于水面时,水面会显示;当地形高于水面时,显示地形。

9. Erosion 类的核心功能

  • createCommand:创建渲染命令,设置顶点数组、着色器程序、渲染状态和统一变量映射。

  • update:更新水面效果的时间参数和帧率,并将渲染命令添加到渲染队列中。

  • destroy:销毁渲染命令,释放资源。

最后通过继承Cesium的Primitive类,以上这些效果可以与现有的地理信息进行结合,实现高度集成的水模拟。

注意

该版本的代码目前在普通电脑上跑性能不是很好,有些卡顿,如果你的电脑有独显,可以开启浏览器的图形加速,就会非常流畅!

最后

好了,如果想参考完整代码,请参考:github.com/tingyuxuan2…

如果想系统学习Cesium,可以了解下我的Cesium系列教程《Cesium从入门到实战》,将Cesium的知识点进行串联,让不了解Cesium的小伙伴拥有一个完整的学习路线,并最终完成一个智慧城市的完整项目,课程也在不断更新迭代中,想了解+作者:brown_7778(备注来意)了解教程细节。

有需要进可视化&Webgis交流群可以加我:brown_7778(备注来意),另外也可接项目合作。