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.ImageLoaderoptions. - 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 lowerwaveHeightfor mobile or low-end devices. Prioritize texture loading usingCesium.Resource.fetchImagewith appropriate priority hints.