Cesium 实现园区水景!3 种水面效果,Water 材质 5 分钟搞定

0 阅读3分钟

之前创建的园区还存在缺陷,本来设计的园区内是有水景效果的,但是一直弄得不是特别顺利。

今天搞出来了,三种方法可以实现水面,大家按需取用。

使用Water材质(最简单)

Cesium其实内置了Water材质,这是一种基于GPU的实时渲染技术,通过法线贴图(Normal Mapping)和菲涅尔效应(Fresnel Effect)模拟水面的波浪运动和反光特性。

image.png

该材质支持动画效果,能实时计算波浪的传播和反射。

我在页面中用到的也是这种方案,相对来说效果不差,而且实现起来比较简单。

唯一的不足之处可能是页面静止状态下看不到波浪效果。

实现代码

const loadWater = () => {
    // 水面坐标点
    const waterPositions = [
        { lon: 117.105362, lat: 36.437808 },
        { lon: 117.105478, lat: 36.437790 },
        { lon: 117.105578, lat: 36.437737 },
    ];

    // 将经纬度坐标转换为 Cartesian3 坐标
    const polygonHierarchy = new Cesium.PolygonHierarchy(
        waterPositions.map(pos => Cesium.Cartesian3.fromDegrees(pos.lon, pos.lat))
    );

    cesiumViewer.value.scene.primitives.add(
        new Cesium.Primitive({
            geometryInstances: new Cesium.GeometryInstance({
                geometry: new Cesium.PolygonGeometry({
                    polygonHierarchy: polygonHierarchy,
                    vertexFormat: Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT,
                    height: 0, // 水面高度,根据实际场景调整
                }),
            }),
            appearance: new Cesium.EllipsoidSurfaceAppearance({
                material: new Cesium.Material({
                    fabric: {
                        type: "Water",
                        uniforms: {
                            baseWaterColor: new Cesium.Color(
                                64 / 255.0,
                                157 / 255.0,
                                253 / 255.0,
                                0.6
                            ),
                            normalMap: "/water.jpg",
                            frequency: 1000.0,
                            animationSpeed: 0.1,
                            amplitude: 10,
                            specularIntensity: 8,
                        },
                    },
                }),
            }),
        })
    );
}

自定义Shader实现高级水面

对于需要更高质量水面效果的场景,我们可以使用自定义Shader实现高级水面渲染。通过WebGL着色器,我们可以精确控制水面的波浪形态、光照计算和材质特性。

但是我不太建议使用这种方案,用起来比较复杂,需要WebGL和着色器的相关内容。

并且性能开销相对较大,并不适合超大规模场景。

class AdvancedWaterMaterial extends Cesium.Material {
    constructor(options) {
        super({
            fabric: {
                type: 'AdvancedWater',
                uniforms: {
                    uTime: 0,
                    uBaseColor: new Cesium.Color(0, 0.4, 0.8, 0.7),
                    uSpecularColor: new Cesium.Color(1, 1, 1, 1),
                    uWaveAmplitude: 0.5,
                    uWaveFrequency: 0.01,
                    uWaveSpeed: 0.1,
                    uFresnelPower: 2.0,
                    uSpecularPower: 100.0
                },
                source: `
                    uniform float uTime;
                    uniform vec4 uBaseColor;
                    uniform vec4 uSpecularColor;
                    uniform float uWaveAmplitude;
                    uniform float uWaveFrequency;
                    uniform float uWaveSpeed;
                    uniform float uFresnelPower;
                    uniform float uSpecularPower;

                    varying vec3 vPosition;
                    varying vec3 vNormal;
                    varying vec3 vEyeDirection;

                    // 简单的波浪函数
                    float waveFunction(vec2 pos, float time) {
                        return sin(pos.x * uWaveFrequency + time * uWaveSpeed) *
                               sin(pos.y * uWaveFrequency + time * uWaveSpeed) *
                               uWaveAmplitude;
                    }

                    // 计算法线
                    vec3 computeNormal(vec3 pos, float time) {
                        float eps = 0.1;
                        vec3 dx = vec3(eps, 0, waveFunction(pos.xz + vec2(eps, 0), time) - waveFunction(pos.xz, time));
                        vec3 dy = vec3(0, eps, waveFunction(pos.xz + vec2(0, eps), time) - waveFunction(pos.xz, time));
                        return normalize(cross(dx, dy));
                    }

                    // 菲涅尔效应
                    float fresnel(vec3 normal, vec3 eyeDirection) {
                        float fresnelTerm = dot(normalize(eyeDirection), normalize(normal));
                        fresnelTerm = clamp(1.0 - fresnelTerm, 0.0, 1.0);
                        return pow(fresnelTerm, uFresnelPower);
                    }

                    void vertexMain(VertexInput vsInput, inout czm_modelVertexOutput vsOutput) {
                        // 修改顶点位置以创建波浪效果
                        float waveHeight = waveFunction(vsInput.positionMC.xz, uTime);
                        vsInput.positionMC += vec3(0, waveHeight, 0);

                        // 计算法线
                        vsOutput.normalEC = computeNormal(vsInput.positionMC, uTime);

                        // 传递变量到片元着色器
                        vPosition = vsInput.positionMC;
                        vNormal = vsOutput.normalEC;
                        vEyeDirection = -vsOutput.positionEC;
                    }

                    void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) {
                        // 基础颜色
                        vec4 baseColor = uBaseColor;

                        // 计算光照
                        vec3 lightDirection = normalize(vec3(1, 1, 1));
                        vec3 normal = normalize(vNormal);
                        float diffuse = max(dot(normal, lightDirection), 0.0);

                        // 计算高光
                        vec3 reflectDirection = reflect(-lightDirection, normal);
                        float specular = pow(max(dot(normalize(vEyeDirection), reflectDirection), 0.0), uSpecularPower);

                        // 菲涅尔效应
                        float fresnelTerm = fresnel(normal, vEyeDirection);

                        // 最终颜色合成
                        material.diffuse = baseColor.rgb * (1.0 - fresnelTerm) + vec3(0.8, 0.9, 1.0) * fresnelTerm;
                        material.specular = uSpecularColor.rgb * specular;
                        material.alpha = baseColor.a;
                    }
                `
            },
            translucent: true
        });

        // 动画更新
        this._time = 0;
        this._animationId = Cesium.requestAnimationFrame(this.update.bind(this));
    }

    update() {
        this._time += 0.016; // 约60fps
        this.setUniform('uTime', this._time);
        this._animationId = Cesium.requestAnimationFrame(this.update.bind(this));
    }
}

// 使用自定义材质加载水面
const loadAdvancedWater = () => {
    // 注册自定义材质
    Cesium.Material._materialCache.addMaterial('AdvancedWater', {
        fabric: {
            type: 'AdvancedWater',
            uniforms: {
                uTime: 0,
                uBaseColor: new Cesium.Color(0, 0.4, 0.8, 0.7),
                uSpecularColor: new Cesium.Color(1, 1, 1, 1),
                uWaveAmplitude: 0.5,
                uWaveFrequency: 0.01,
                uWaveSpeed: 0.1,
                uFresnelPower: 2.0,
                uSpecularPower: 100.0
            },
            source: `
                // 着色器代码
            `
        },
        translucent: true
    });

    const waterPositions = [
        // 水面坐标点
    ];

    const polygonHierarchy = new Cesium.PolygonHierarchy(
        waterPositions.map(pos => Cesium.Cartesian3.fromDegrees(pos.lon, pos.lat))
    );

    cesiumViewer.value.scene.primitives.add(
        new Cesium.Primitive({
            geometryInstances: new Cesium.GeometryInstance({
                geometry: new Cesium.PolygonGeometry({
                    polygonHierarchy: polygonHierarchy,
                    vertexFormat: Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT,
                    height: 0,
                }),
            }),
            appearance: new Cesium.EllipsoidSurfaceAppearance({
                material: new AdvancedWaterMaterial()
            }),
        })
    );
};

模型实现动态水面

对于一些特定场景,我们可以使用建模软件创建精细的水面模型,然后导出为glTF格式在Cesium中使用。

image.png

这种方法适合需要高度细节的静态水面效果,尤其适合建模比较好的同学。

总结

对于大多数场景,Water材质已经能够满足需求。最关键的是简单,仅需几行代码就能实现带波浪动画、反光效果的水面。

性能开销低,唯一短板是静止状态下波浪效果不明显,可通过调整frequency/amplitude参数优化。

对于需要高质量效果的项目,自定义Shader是更好的选择。

而对于静态场景或精细动画,Model模型方法则更合适。(需要建模知识)