Implementing Animated River Effects in 3D GIS Using Dynamic Shaders

A dynamic river effect in a 3D geographic information scene can be created by applying a custom animated material to a polyline geometry. This approach allows for realistic water flow visualization along arbitrary paths.

Core Implementation Strategy

  1. Define a custom material property that supports a moving texture.
  2. Create a polyline entity using geographic coordinates to represent the river path.
  3. Apply the custom animated material to the polyline.
  4. Dynamically adjust the polyline's pixel width based on camera altitude to maintain visual scale.

The primary technical challenge is developing a custom material that animates a texture across the polyline. The following code defines such a material.

function AnimatedRiverMaterial(riverColor, animationDuration) {
  this._definitionChangedEvent = new Cesium.Event();
  this._riverColor = riverColor;
  this._animationDuration = animationDuration;
  this._animationStartTime = (new Date()).getTime();
}

Object.defineProperties(AnimatedRiverMaterial.prototype, {
  isConstant: {
    get: function () { return false; }
  },
  definitionChanged: {
    get: function () { return this._definitionChangedEvent; }
  },
  color: Cesium.createPropertyDescriptor('color')
});

AnimatedRiverMaterial.prototype.getType = function () {
  return 'AnimatedRiver';
};

AnimatedRiverMaterial.prototype.getValue = function (time, output) {
  if (!Cesium.defined(output)) {
    output = {};
  }
  output.color = Cesium.Property.getValueOrClonedDefault(this._riverColor, time, Cesium.Color.WHITE, output.color);
  output.texture = Cesium.Material.AnimatedRiverTexture;
  output.timeElapsed = (((new Date()).getTime() - this._animationStartTime) % this._animationDuration) / this._animationDuration;
  return output;
};

AnimatedRiverMaterial.prototype.equals = function (otherMaterial) {
  return this === otherMaterial ||
    (otherMaterial instanceof AnimatedRiverMaterial &&
    Cesium.Property.equals(this._riverColor, otherMaterial._riverColor));
};

// Register the custom material type with Cesium.
Cesium.AnimatedRiverMaterial = AnimatedRiverMaterial;
Cesium.Material.AnimatedRiverType = 'AnimatedRiver';
Cesium.Material.AnimatedRiverTexture = "./assets/waterFlow.png";

Cesium.Material.AnimatedRiverShaderSource = `
uniform vec4 color;
uniform float timeElapsed;
uniform sampler2D texture;

czm_material czm_getMaterial(czm_materialInput materialInput) {
  czm_material material = czm_getDefaultMaterial(materialInput);
  vec2 textureCoords = materialInput.st;
  // Animate texture along the S coordinate (polyline direction)
  vec4 sampledTexture = texture2D(texture, vec2(fract(textureCoords.s * 5.0 - timeElapsed), textureCoords.t));
  material.alpha = sampledTexture.a * color.a;
  material.diffuse = sampledTexture.rgb;
  return material;
}`;

Cesium.Material._materialCache.addMaterial(Cesium.Material.AnimatedRiverType, {
  fabric: {
    type: Cesium.Material.AnimatedRiverType,
    uniforms: {
      color: new Cesium.Color(1.0, 0.0, 0.0, 0.5),
      texture: Cesium.Material.AnimatedRiverTexture,
      timeElapsed: 0
    },
    source: Cesium.Material.AnimatedRiverShaderSource
  },
  translucent: function (material) {
    return true;
  }
});

With the material defined, a polyline entity is created to represent the river's course. The positions are provided as an array of longitudes, latitudes, and heights.

var watercourse = viewer.entities.add({
  name: 'riverFeature',
  polyline: {
    positions: Cesium.Cartesian3.fromDegreesArrayHeights(coordinateArray),
    width: 25,
    followSurface: true,
    material: new Cesium.AnimatedRiverMaterial(Cesium.Color.AQUA, 8000),
    clampToGround: true,
    distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 25000)
  }
});

The default polyline width is a fixed pixel value, which does not scale realistically as the camera zooms in or out. To create a more authentic sense of scale, the width can be adjusted dynamically based on the viewer's altitude.

var widthBase = 30;
var widthDecrement = 3;
var altitudeThreshold = 4000;

scene.postRender.addEventListener(function() {
  var cameraPos = viewer.scene.camera.position;
  var cameraCartographic = Cesium.Cartographic.fromCartesian(cameraPos);
  var currentAltitude = cameraCartographic.height;

  if (currentAltitude < altitudeThreshold) {
    watercourse.polyline.width = widthBase;
  } else {
    // Decrease width in steps as altitude increases
    var step = Math.floor((currentAltitude - altitudeThreshold) / 1000);
    var newWidth = widthBase - (widthDecrement * step);
    newWidth = Math.max(newWidth, 2); // Ensure a minimum width
    watercourse.polyline.width = newWidth;
  }
});

Tags: 3D GIS WebGL cesium Data Visualization Shader Programming

Posted on Thu, 27 Aug 2026 16:13:05 +0000 by lukevrn