大家好,我是日拱一卒的攻城师不浪,致力于技术与艺术的融合。这是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_fadedepth、large_waveheight、small_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_wavesize、small_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
- 计算光照的高光部分,
specular和specular2分别用于软高光和光滑高光,模拟光线在水面上的反射。
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. 片段着色器
在片段着色器中,水体的颜色通过多个因素混合:
-
水体颜色:使用
watercolor、watercolor2、water_specularcolor等控制水面的颜色和反射。 -
渐变效果:根据海岸线到水的深度,混合不同的颜色来表示浅水和深水区域。
-
光照和阴影:计算水面的高光、阴影等效果,模拟水面反射的真实感。
8. 水面和地形的结合
在着色器中,水面高度(level)会根据高度图和动态波浪变化进行调整。当地形的高度低于水面时,水面会显示;当地形高于水面时,显示地形。
9. Erosion 类的核心功能
-
createCommand:创建渲染命令,设置顶点数组、着色器程序、渲染状态和统一变量映射。
-
update:更新水面效果的时间参数和帧率,并将渲染命令添加到渲染队列中。
-
destroy:销毁渲染命令,释放资源。
最后通过继承Cesium的Primitive类,以上这些效果可以与现有的地理信息进行结合,实现高度集成的水模拟。
注意
该版本的代码目前在普通电脑上跑性能不是很好,有些卡顿,如果你的电脑有独显,可以开启浏览器的图形加速,就会非常流畅!
最后
好了,如果想参考完整代码,请参考:github.com/tingyuxuan2…
如果想系统学习Cesium,可以了解下我的Cesium系列教程
《Cesium从入门到实战》,将Cesium的知识点进行串联,让不了解Cesium的小伙伴拥有一个完整的学习路线,并最终完成一个智慧城市的完整项目,课程也在不断更新迭代中,想了解+作者:brown_7778(备注来意)了解教程细节。
有需要进
可视化&Webgis交流群可以加我:brown_7778(备注来意),另外也可接项目合作。