告别静态水面的平庸!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)

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

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

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

相关推荐
不浪brown10 小时前
丝滑!Cesium中实现机械模型动作仿真全流程
cesium
duansamve3 天前
Cesium性能优化
cesium
一梦、んんん4 天前
cesium FBO(一)渲染到纹理(RTT)
cesium
青山Coding4 天前
Cesium基础(七):Camera(相机)常用的API及飞行漫游
gis·cesium
YGY_Webgis糕手之路4 天前
Cesium 快速入门(十) JulianDate(儒略日期)详解
前端·gis·cesium
YGY_Webgis糕手之路4 天前
Cesium 快速入门(三)Viewer:三维场景的“外壳”
前端·gis·cesium
YGY_Webgis糕手之路5 天前
Cesium 快速入门(十二)数据加载详解
前端·gis·cesium
YGY_Webgis糕手之路5 天前
Cesium 快速入门(十三)事件系统
前端·gis·cesium
duansamve5 天前
Cesium快速入门到精通系列教程十六:材质系统
cesium
YGY_Webgis糕手之路5 天前
Cesium 快速入门(六)实体类型介绍
前端·gis·cesium