告别静态水面的平庸!Cesium中实现水面倒影+动态流向的逼真水特效

大家好,我是日拱一卒的攻城师不浪,致力于前沿科技探索,摸索小而美工作室,这是2025年输出的第36/100篇原创文章。

效果展示

【告别静态水面的平庸!Cesium中实现水面倒影+动态流向的逼真水特效】 www.bilibili.com/video/BV1bs...

前言

看了很多Cesium的水效果,很多都是敷衍了事,例如:

用矩形颜色面模拟

简单的自定义材质

而真正用心去做了的效果,如下:

带流向以及水面倒影的水材质

其实如果不是真正的水利项目,大概不会有人花时间去在水效果的渲染上下功夫。

但是,在三维场景中,提升水面效果是非常有助于提升场景真实感以及美观度的,还能提升自己的专业性。

所以,今天给大家分享一个在Cesium中实现水面倒影,一起来探索如何创建具有流动感反射效果的水面。

技术原理解析

1. 反射纹理捕获

核心原理是通过创建一个反转的摄像机视角,将场景渲染到纹理中,然后将该纹理应用到水面上。具体步骤:

  • 基于当前摄像机位置,计算出关于水平面对称的反射摄像机位置

  • 使用该反射摄像机渲染场景到FrameBuffer

  • 将渲染结果作为纹理应用到水面

2. 法线贴图和水面动态效果

  • 使用法线贴图(Normal Map)模拟水面的波纹效果

  • 通过时间因子和流动方向参数,使法线贴图产生动态流动的效果

  • 根据视角和法线的关系计算反射强度,实现更真实的水面效果

3. 自定义图元

通过扩展CesiumPrimitive类创建自定义图元,实现水面的渲染。

核心代码解析

新建水面反射类

新建WaterFacePrimitive并继承Cesium.Primitive,以便于后续直接使用该类;

java 复制代码
class WaterFacePrimitive extends Primitive {
}

反射纹理更新逻辑

以下代码展示了如何创建反射相机更新反射纹理

kotlin 复制代码
updateReflectTexture(frameState) {
    const {context, camera} = frameState
    
    // 计算模型视图矩阵和投影矩阵
    this.modelViewMatrix = Matrix4.multiply(camera.viewMatrix, this.modelMatrix, this.modelViewMatrix);
    this.modeiViewProjection = Matrix4.multiply(
        camera.frustum.projectionMatrix,
        this.modelViewMatrix,
        this.modeiViewProjection
    );
    
    // 计算反射平面
    const centerPos = Cartesian3.clone(this._waterCenterPos);
    const car3 = new Cartesian3();
    Cartesian3.normalize(centerPos, car3);
    const plane = Plane.fromPointNormal(center, normal);
    
    // 计算反射矩阵
    const dot = -Cartesian3.dot(normal, center);
    const matrix = new Matrix4(
        -2 * normal.x * normal.x + 1,
        -2 * normal.x * normal.y,
        -2 * normal.x * normal.z,
        -2 * normal.x * dot,
        // ...其余矩阵元素
    );
    
    // 计算反射相机的方向和位置
    // ...计算方向和位置的代码
    
    // 更新反射相机参数
    this._reflectCamera.direction = direction_m;
    // ...其他参数更新
    
    // 清除反射纹理
    const clear = new Cesium.ClearCommand({
        color: ClearCommand.Color.fromBytes(14, 33, 60, 255),
        depth: 1,
        framebuffer: this._reflectPassState.framebuffer,
    });
    clear.execute(context, this._reflectPassState);
    
    // 使用反射相机渲染场景到纹理
    this.updateTexture(frameState, this._reflectPassState, this._reflectCamera);
}

着色器代码分析

水面效果主要通过两个着色器实现:

顶点着色器处理基本的顶点变换并计算纹理坐标:

ini 复制代码
in vec3 position;
in vec2 st;
uniform mat4 u_modelViewProjectionMatrix;
// ...其他uniform变量
out vec3 eyeDir;
out vec2 texCoord;
out float myTime;
out vec4 projectionCoord;

void main(void)
{
  gl_Position = u_modelViewProjectionMatrix * vec4(position.xyz,1.0);
  // 计算视线方向
  if (u_clampToGroud == 1)
  {
    eyeDir = (u_camPosition - position.xyz) * u_scale;
  } else {
    vec4 pos = u_modelViewMatrix * vec4(position.xyz,1.0);
    eyeDir = vec3(u_invWorldViewMatrix*vec4(pos.xyz,0.0));
    projectionCoord = gl_Position;
  }
  texCoord = (st+u_texCoordOffset)*u_texCoordScale;
  myTime = 0.01 * u_frameTime;
}

片段着色器实现水面的波纹效果和反射效果:

ini 复制代码
uniform sampler2D u_normalMap;
uniform sampler2D u_refractMap;
uniform sampler2D u_reflectMap;
// ...其他uniform变量
in vec3 eyeDir;
in vec2 texCoord;
in float myTime;
in vec4 projectionCoord;

void main (void)
{
  float texScale = 35.0;
  float texScale2 = 10.0;

  // 计算法线
  vec2 mytexFlowCoord = texCoord * texScale;
  // ...法线计算代码

  // 计算反射颜色
  vec3 envColor = u_reflectColor.rgb;
  if (u_reflection == 1)
  {
      vec2 final = projectionCoord.xy / projectionCoord.w;
      final = final * 0.5 + 0.5;
      final.y = 1.0 - final.y;
      envColor = texture(u_reflectMap, final + myNormal.xy/texScale2*transp).rgb;
  }

  // 计算最终颜色
  myangle = dot(myNormal,normalize(eyeDir));
  myangle = 0.95-0.6*myangle*myangle;
  vec3 base = u_refractColor.rgb;
  if (u_useRefractTex == 1)
      base = texture(u_refractMap,(texCoord + myNormal.xy/texScale2*0.03*transp)*32.0).rgb;
  base = mix(base, u_waterColor.rgb, u_waterColor.a);
  out_FragColor = vec4(mix(base, envColor, myangle*transp), 1.0);
}

使用示例

Vue组件中使用水面效果:

php 复制代码
const create = () => {
    const Cartesian3 = Cesium.Cartesian3;
    const pos = [
        new Cartesian3(-2892764.2195214387, 4724482.332476368, 3150384.8106400613),
        new Cartesian3(-2892903.3088703067, 4724423.505708753, 3150345.575495131),
        new Cartesian3(-2892903.277250517, 4724473.075561376, 3150271.7631767085),
        new Cartesian3(-2892765.5659396723, 4724547.883313923, 3150285.9349773265),
    ];
    const polygon = new Cesium.PolygonGeometry({
        polygonHierarchy: new Cesium.PolygonHierarchy(pos),
        perPositionHeight: false,
        height: 0,
    });
    const water = new WaterFacePrimitive({
        waterPolygon: polygon,
        flowSpeed: 2,
    });
    __viewer.scene.primitives.add(water);
};

最后

实现以上效果能够大大提升三维场景的真实感和沉浸感,特别适用于需要展示水体的水利孪生系统、智慧城市、景观规划等应用场景。

【不浪开源的Cesium案例集合】:github.com/tingyuxuan2...

水面倒影的完整源码都放在了不浪的Cesium教程里,想系统学习Cesium的小伙伴儿,可以了解下不浪的教程《Cesium从入门到实战》,将Cesium的知识点进行串联,让不了解Cesium的小伙伴拥有一个完整的学习路线,并最终完成一个智慧城市的完整项目,课程最近也更新了很多进阶内容,想了解课程大纲,+作者:brown_7778(备注来意)。
另外有需要进可视化&Webgis交流群可以加我:brown_7778(备注来意),也欢迎三维可视化领域的交流合作。


👇🏻不浪往期编程干货

往期推荐

Cesium最全系列教程!从零到一完成智慧城市实战项目!

别再只会2D打点了!Cesium中通过顶点绘制构造酷炫的3D打点(棱锥体)

【开源】Cesium场景效果大全(cesium+vue3+vite+vuex)

不懂这些GIS基础,开发Cesium寸步难行!

Cesium中常用到的5种相机定位方法详解,每种都适用于不同的场景

threejs实战数字孪生园区开源(threejs+vue3+vite)

点击关注👇,底部菜单领开源资料

长按识别个人微信,交流合作

本文使用 文章同步助手 同步

相关推荐
一个有理想的摸鱼选手7 天前
CesiumLite-开箱即用的轻量化三维地图包(持续更新中...)
gis·cesium
不浪brown17 天前
重磅开源!Cesium实现高度雾仿真,谁再说Cesium做不出好效果?
cesium
青山Coding18 天前
Cesium基础(五)实体创建与拖拽
前端·cesium
Mapmost21 天前
全新升级!3DTiles加载速度Mapmost完胜Cesium
性能优化·webgl·cesium
汪洪墩22 天前
使用Mars3d加载热力图的时候,出现阴影碎片
开发语言·前端·javascript·vue.js·cesium
springfe010123 天前
Cesium 3D地图 图元 圆柱 图片实现
前端·cesium
不浪brown25 天前
浏览器3D渲染卡成PPT?6个性能优化指标,你都知道吗?
three.js·cesium
GIS之家1 个月前
vue+cesium示例:3D热力图(附源码下载)
前端·vue.js·3d·cesium·webgis·3d热力图
不浪brown1 个月前
开源!矢量建筑白模泛光特效以及全国77个大中城市的矢量shp数据获取!
前端·cesium