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
- Define a custom material property that supports a moving texture.
- Create a polyline entity using geographic coordinates to represent the river path.
- Apply the custom animated material to the polyline.
- 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;
}
});