Real-Time Animated Water Flow Visualization with CesiumJS

1. Viewer Initialization

<link href="path/to/cesium/Widgets/widgets.css" rel="stylesheet">
<script src="path/to/cesium/Cesium.js"></script>
<div id="cesiumContainer" style="width:100%;height:100vh;"></div>

<script>
const cesiumViewer = new Cesium.Viewer('cesiumContainer', {
  terrainProvider: Cesium.createWorldTerrain(),
  baseLayerPicker: false,
  scene3DOnly: true
});
</script>

2. Custom Animated Water Material

This implementation uses a two-phase wave displacement technique combined with procedural noise sampling to simulate natural water flow across terrain.

const animatedWaterMaterial = new Cesium.Material({
  fabric: {
    type: 'AnimatedLiquidSurface',
    uniforms: {
      flowDirectionMap: './flow_directions.png',
      surfaceNormals: './water_bump.png',
      turbulenceMap: './perlin_noise.png',
      flowSpeed: 0.45,
      waveHeight: 4.2,
      timeOffset: 0.0
    },
    source: `
      uniform sampler2D flowDirectionMap;
      uniform sampler2D surfaceNormals;
      uniform sampler2D turbulenceMap;
      uniform float flowSpeed;
      uniform float waveHeight;
      uniform float timeOffset;

      varying vec2 v_st;
      varying vec3 v_positionEC;

      float sampleNoise(vec2 uv) {
        return texture2D(turbulenceMap, uv * 8.0).r;
      }

      void main() {
        // Decode flow direction from RG channels
        vec2 direction = texture2D(flowDirectionMap, v_st).rg * 2.0 - 1.0;
        direction *= flowSpeed;

        // Two-phase temporal offset for smooth looping
        float t0 = fract(timeOffset * 0.12);
        float t1 = fract(timeOffset * 0.12 + 0.47);

        vec2 uvShiftA = v_st * 12.0 + direction * t0;
        vec2 uvShiftB = v_st * 12.0 + direction * t1;

        // Sample normal maps at both offsets
        vec3 normalA = texture2D(surfaceNormals, uvShiftA).rgb;
        vec3 normalB = texture2D(surfaceNormals, uvShiftB).rgb;

        // Blend based on phase distance for continuity
        float blendFactor = abs(0.5 - t0) * 2.0;
        vec3 blendedNormal = mix(normalA, normalB, blendFactor);

        // Add high-frequency turbulence
        float noiseVal = sampleNoise(v_st * 20.0 + timeOffset * 0.7);
        blendedNormal.xy += vec2(noiseVal * 0.03);

        // Lighting calculation
        vec3 viewVec = normalize(-v_positionEC);
        float reflectance = pow(max(dot(viewVec, blendedNormal), 0.0), 24.0);
        vec3 baseTint = vec3(0.08, 0.25, 0.72);
        gl_FragColor = vec4(mix(baseTint, vec3(0.95), reflectance), 0.78);
      }
    `
  }
});

3. Geometry Binding and Runtime Updates

The material is assigned to a ground primitive covering a specified geographic region. Time uniforms are updated each frame to drive animation.

// Define target area (e.g., coastal zone)
const regionBounds = Cesium.Rectangle.fromDegrees(-74.5, 40.2, -73.8, 40.9);

// Construct geometry instance
const waterInstance = new Cesium.GeometryInstance({
  geometry: new Cesium.RectangleGeometry({
    rectangle: regionBounds,
    vertexFormat: Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT
  })
});

// Create ground primitive with custom appearance
const waterSurface = new Cesium.GroundPrimitive({
  geometryInstances: waterInstance,
  appearance: new Cesium.EllipsoidSurfaceAppearance({
    material: animatedWaterMaterial,
    aboveGround: true
  })
});

// Add to scene
cesiumViewer.scene.primitives.add(waterSurface);

// Update time uniform every frame
cesiumViewer.scene.preUpdate.addEventListener(() => {
  const now = Cesium.JulianDate.now();
  animatedWaterMaterial.uniforms.timeOffset = now.secondsOfDay;
});

4. Implementation Notes

  • Texture Preparation: Flow direction textures must be authored with normalized 2D vectors stored in red-green channels. Use lossless formats (e.g., PNG) and disable GPU compression during loading via Cesium.ImageLoader options.
  • Animation Stability: Dual-phase sampling eliminates visual popping by ensuring continouus interpolation between displaced UV coordinates.
  • Performance Guidance: Reduce normal map tiling frequency (v_st * N) and lower waveHeight for mobile or low-end devices. Prioritize texture loading using Cesium.Resource.fetchImage with appropriate priority hints.

Tags: CesiumJS WebGL shader flowmap terrain-rendering

Posted on Wed, 13 May 2026 23:28:00 +0000 by rbarnett