Cesium-气象要素PNG色斑图叠加

22 阅读8分钟

1、PNG色斑图渲染原理

  • PNG的R通道存储要素的像素值0-255
  • 构建256色查找表,将颜色查找表转换为cesium可用纹理
  • ceaium渲染中纹理采样选择最邻近采样,边界清晰

2、实现步骤

(1)生成256色查找表,使用反归一化,将像素值转为实际值

反归一化公式: 实际值 = 归一化值 * (最大值 - 最小值)+ 最小值

映射过程 (i / 255)将像素索引归一化到[0, 1]区间 如i= 128 -> 128/255 ≈ 0.5

代码的实现

/**
 * 生成256色查找表
 * @param {Object} params - 参数
 * @param {string} params.colorInfo - 色卡信息,如 "[[255,0,0],[0,255,0],[0,0,255]]"
 * @param {string} params.colorLevel - 色阶,如 "-10,0,10,20,30,40"
 * @param {number} params.minValue - 数据最小值
 * @param {number} params.maxValue - 数据最大值
 * @returns {Uint8Array} 颜色查找表 (256x4 RGBA)
 */
generateColorLUT({colorInfo, colorLevel, minValue, maxValue}) {

    // 解析色卡
    let colors = colorInfo.split('],').map(ele => ele.trim()).map(ele => ele.replace('[', '').replace(']', ''));
    colors = colors.map(ele => ele.split(',').map(Number));

    // 解析色阶
    let levels = colorLevel.split(',').map(Number);

    // 构建颜色区间
    const colorRange = []
    for (let i = 0; i < levels.length - 1; i++) {
        colorRange.push({
            min: levels[i],
            max: levels[i + 1],
            color: colors[i] || colors[i - 1],
        });
    }

    // 生成256色查询表(RGBA格式)
    let lut = new Uint8Array(256 * 4); // 256个颜色,每个RGBA 4个字节

    for (let i = 0; i < 256; i++) {
        const idx = i * 4

        // 索引0为透明背景
        if (i === 0) {
            lut[idx] = 0;      // R
            lut[idx + 1] = 0;  // G
            lut[idx + 2] = 0;  // B
            lut[idx + 3] = 0;  // A (透明)
            continue;
        }

        // 将像素索引(0-255)反归一化为实际值
        const tempValue = (i / 255) * (maxValue - minValue) + minValue;

        // 查找对应的颜色区间
        let selectedColor = colors[colors.length -1]; // 默认最大值颜色

        for (let j = 0; j < colorRange.length; j++) {
            const range = colorRange[j];

            if (tempValue >= range.min && tempValue < range.max) {
                selectedColor = range.color
                break;
            }

        }

        // 设置颜色 (完全不透明)
        lut[idx] = selectedColor[0] // R
        lut[idx + 1] = selectedColor[1] // G
        lut[idx + 2] = selectedColor[2] // B
        lut[idx + 3] = 255 // A (不透明)

    }

    return lut;

}
(2)将颜色查找表转换为cesium可用纹理 Uint8Array -> HTMLCanvasElement
/**
 * 从数组创建纹理
 * @param {Uint8Array} data - RGBA数据
 * @param {number} width - 宽度
 * @param {number} height - 高度
 * @returns {HTMLCanvasElement} Canvas纹理
 */
createTextureFromArray(data, width, height) {
    // 创建canvas
    const canvas = document.createElement('canvas');
    canvas.width = width; // 通常是256
    canvas.height = height; // 通常是1

    // 获取2D绘图上下文
    const ctx = canvas.getContext('2d');

    // 创建空的ImageData对象--ImageData是canvas原生支持的像素格式数据
    const imageData = ctx.createImageData(width, height);
    // imageData.data 是一个Unit8ClampedArray,长度为width * height * 4

    // 复制数据--将Uint8Array数据复制到ImageData中
    for (let i = 0; i < data.length; i++) {
        imageData.data[i] = data[i];
    }

    // 将ImageData绘制到Canvas上
    ctx.putImageData(imageData, 0, 0);

    return canvas;
}

输出为

image.png

(3)加载图片数据
/**
 * 加载图片
 * @param {string} url - 图片URL或者
 * @returns {Promise<HTMLImageElement>}
 */
loadImage(url) {
    return new Promise((resolve, reject) => {
        const img = new Image();
        img.crossOrigin = 'anonymous'; // 设置跨域属性
        img.onload = () => resolve(img);
        img.onerror = reject;
        img.src = url;
    })
}
(4)创建材质

最近邻采样 (Nearest Neighbor):边界锐利,使用四舍五入

// GLSL实现
float index = pixel.r * 255.0;
float roundedIndex = floor(index + 0.5);  // 四舍五入到最近整数
float texCoord = (roundedIndex + 0.5) / 256.0;
vec4 color = texture2D(colorLUT, vec2(texCoord, 0.5));

image.png

在着色器中使用最邻近采样(另一篇文章有解释最邻近采样和线性插值,有兴趣的可以看一下)

/**
 * 获取着色器源码
 * @returns {string} GLSL着色器代码
 */
getShaderSource() {
    return `
  uniform sampler2D dataTexture;    // 数据纹理 (PNG, R通道存储温度值)
  uniform sampler2D colorLUT;        // 颜色查找表 (256x1)
  uniform float minTemp;             // 温度最小值
  uniform float maxTemp;             // 温度最大值
  uniform float opacity;             // 透明度
  uniform float visible;              // 可见性
  
  czm_material czm_getMaterial(czm_materialInput materialInput) {
    czm_material material = czm_getDefaultMaterial(materialInput);
    
    // 如果不可见,返回透明
    if (visible < 0.5) {
      material.alpha = 0.0;
      return material;
    }
    
    // 采样数据纹理
    vec4 pixel = texture(dataTexture, materialInput.st);
    
    // 透明像素直接返回
    if (pixel.a < 0.01) {
      material.alpha = 0.0;
      return material;
    }
    
    // 【关键】最近邻采样 - 避免颜色混合
    // pixel.r 范围 0.0-1.0,乘以255得到0-255的索引
    float index = pixel.r * 255.0;
    // 四舍五入到最近的整数索引
    float roundedIndex = floor(index + 0.5);
    // 转换为纹理坐标 (0-1),偏移0.5到像素中心
    float texCoord = (roundedIndex + 0.5) / 256.0;
    
    // 从查找表获取颜色
    vec4 color = texture(colorLUT, vec2(texCoord, 0.5));
    
    // 计算实际温度值 (用于调试,但不影响渲染)
    // float temperature = roundedIndex / 255.0 * (maxTemp - minTemp) + minTemp;
    
    material.diffuse = czm_gammaCorrect(color.rgb);
    material.alpha = color.a * opacity;
    
    return material;
  }
`;
}

创建材质

//创建材质
this.material = new Cesium.Material({
    fabric: {
        type: 'material',
        uniforms: {
            dataTexture: dataImage,
            colorLUT: lutTexture,
            minValue: minValue,
            maxValue: maxValue,
            opacity: opacity / 100,
            visible: 1.0
        },
        source: this.getShaderSource()
    }
});
(5)Cesium中创建一个矩形几何体并将其添加到3D场景中
// 创建矩形范围-   extend数组包含4个值:[西经, 南纬, 东经, 北纬]
const rectangle = Cesium.Rectangle.fromDegrees(
    extend[0], extend[1], extend[2], extend[3]
);

// 创建几何体
const geometry = new Cesium.RectangleGeometry({
    rectangle: rectangle,
    vertexFormat: Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT,
    height: 0
});

// 创建几何体实例
const instance = new Cesium.GeometryInstance({
    id: id,
    geometry: geometry
});

// 创建外观
const appearance = new Cesium.EllipsoidSurfaceAppearance({
    material: this.material,
    translucent: true,
    flat: true,
    aboveGround: true
});

// 创建Primitive(使用普通Primitive替代GroundPrimitive,避免地形细分产生的接缝,我页面上用了地形,会出现接缝)
this.layer = new Cesium.Primitive({
    geometryInstances: instance,
    appearance: appearance,
    asynchronous: false
});

// 添加到场景
this.viewer.scene.primitives.add(this.layer);

Primitive与GroundPrimitive的区别 image.png

3、最终实现效果:

image.png

4、所有代码

StaticLayer.js

import * as Cesium from "cesium";

class StaticLayers {

    constructor(viewer) {
        this.viewer = viewer;
        this.imageLayers = [];
        this.layer = null;
        this.isVisible = false;

    }

    /**
     * 生成256色查找表
     * @param {Object} params - 参数
     * @param {string} params.colorInfo - 色卡信息,如 "[[255,0,0],[0,255,0],[0,0,255]]"
     * @param {string} params.colorLevel - 色阶,如 "-10,0,10,20,30,40"
     * @param {number} params.minValue - 数据最小值
     * @param {number} params.maxValue - 数据最大值
     * @returns {Uint8Array} 颜色查找表 (256x4 RGBA)
     */
    generateColorLUT({colorInfo, colorLevel, minValue, maxValue}) {

        // 解析色卡
        let colors = colorInfo.split('],').map(ele => ele.trim()).map(ele => ele.replace('[', '').replace(']', ''));
        colors = colors.map(ele => ele.split(',').map(Number));

        // 解析色阶
        let levels = colorLevel.split(',').map(Number);

        // 构建颜色区间
        const colorRange = []
        for (let i = 0; i < levels.length - 1; i++) {
            colorRange.push({
                min: levels[i],
                max: levels[i + 1],
                color: colors[i] || colors[i - 1],
            });
        }

        console.log('colorRange--', colorRange)

        // 生成256色查询表(RGBA格式)
        let lut = new Uint8Array(256 * 4); // 256个颜色,每个RGBA 4个字节

        for (let i = 0; i < 256; i++) {
            const idx = i * 4

            // 索引0为透明背景
            if (i === 0) {
                lut[idx] = 0;      // R
                lut[idx + 1] = 0;  // G
                lut[idx + 2] = 0;  // B
                lut[idx + 3] = 0;  // A (透明)
                continue;
            }

            // 将像素索引(0-255)反归一化为实际值
            const tempValue = (i / 255) * (maxValue - minValue) + minValue;

            // 查找对应的颜色区间
            let selectedColor = colors[colors.length -1]; // 默认最大值颜色

            for (let j = 0; j < colorRange.length; j++) {
                const range = colorRange[j];

                if (tempValue >= range.min && tempValue < range.max) {
                    selectedColor = range.color
                    break;
                }

            }

            // 设置颜色 (完全不透明)
            lut[idx] = selectedColor[0] // R
            lut[idx + 1] = selectedColor[1] // G
            lut[idx + 2] = selectedColor[2] // B
            lut[idx + 3] = 255 // A (不透明)

        }

        return lut;

    }

    /**
     * 从数组创建纹理
     * @param {Uint8Array} data - RGBA数据
     * @param {number} width - 宽度
     * @param {number} height - 高度
     * @returns {HTMLCanvasElement} Canvas纹理
     */
    createTextureFromArray(data, width, height) {
        // 创建canvas
        const canvas = document.createElement('canvas');
        canvas.width = width; // 通常是256
        canvas.height = height; // 通常是1

        // 获取2D绘图上下文
        const ctx = canvas.getContext('2d');

        // 创建空的ImageData对象--ImageData是canvas原生支持的像素格式数据
        const imageData = ctx.createImageData(width, height);
        // imageData.data 是一个Unit8ClampedArray,长度为width * height * 4

        // 复制数据--将Uint8Array数据复制到ImageData中
        for (let i = 0; i < data.length; i++) {
            imageData.data[i] = data[i];
        }

        // 将ImageData绘制到Canvas上
        ctx.putImageData(imageData, 0, 0);

        return canvas;
    }

    /**
     * 加载图片
     * @param {string} url - 图片URL或者
     * @returns {Promise<HTMLImageElement>}
     */
    loadImage(url) {
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.crossOrigin = 'anonymous'; // 设置跨域属性
            img.onload = () => resolve(img);
            img.onerror = reject;
            img.src = url;
        })
    }

    /**
     * 获取着色器源码
     * @returns {string} GLSL着色器代码
     */
    getShaderSource() {
        return `
      uniform sampler2D dataTexture;    // 数据纹理 (PNG, R通道存储温度值)
      uniform sampler2D colorLUT;        // 颜色查找表 (256x1)
      uniform float minTemp;             // 温度最小值
      uniform float maxTemp;             // 温度最大值
      uniform float opacity;             // 透明度
      uniform float visible;              // 可见性
      
      czm_material czm_getMaterial(czm_materialInput materialInput) {
        czm_material material = czm_getDefaultMaterial(materialInput);
        
        // 如果不可见,返回透明
        if (visible < 0.5) {
          material.alpha = 0.0;
          return material;
        }
        
        // 采样数据纹理
        vec4 pixel = texture(dataTexture, materialInput.st);
        
        // 透明像素直接返回
        if (pixel.a < 0.01) {
          material.alpha = 0.0;
          return material;
        }
        
        // 【关键】最近邻采样 - 避免颜色混合
        // pixel.r 范围 0.0-1.0,乘以255得到0-255的索引
        float index = pixel.r * 255.0;
        // 四舍五入到最近的整数索引
        float roundedIndex = floor(index + 0.5);
        // 转换为纹理坐标 (0-1),偏移0.5到像素中心
        float texCoord = (roundedIndex + 0.5) / 256.0;
        
        // 从查找表获取颜色
        vec4 color = texture(colorLUT, vec2(texCoord, 0.5));
        
        // 计算实际温度值 (用于调试,但不影响渲染)
        // float temperature = roundedIndex / 255.0 * (maxTemp - minTemp) + minTemp;
        
        material.diffuse = czm_gammaCorrect(color.rgb);
        material.alpha = color.a * opacity;
        
        return material;
      }
    `;
    }


    /**
     * 添加色斑图
     * @param {Object} options - 配置选项
     * @param {string} options.data - base64 (R通道存储温度值)
     * @param {Array<number>} options.extend - 地理范围 [西经, 南纬, 东经, 北纬]
     * @param {number} options.min_value - 最小值
     * @param {number} options.max_value - 最大值
     * @param {string} options.colorParams - 色卡信息
     * @param {number} options.opacity - 透明度 0-100,默认100
     * @param {string} options.id - 图层ID(可选)
     * @returns {Promise<Cesium.GroundPrimitive>} 返回创建的Primitive
     */
    async addLayer(options) {
        const {
            data,
            extend,
            min_value,
            max_value,
            colorParams,
            opacity = 100,
            id = 'temperature-layer'
        } = options;

        const minValue =  Number(min_value);
        const maxValue =  Number(max_value);

        const {
            colorInfo,
            colorLevel,
            dataType,
            code,
            elementName
        } = colorParams

        // 生成颜色查找表
        const lutData = this.generateColorLUT({colorInfo, colorLevel, minValue, maxValue});

        // 创建查找表纹理(256 * 1)--目的将颜色查找表数据转换为cesium可用纹理 unit8Array --> HTMLCanvasElement
        const lutTexture = this.createTextureFromArray(lutData, 256, 1)

        // 加载图片数据
        const dataImage = await this.loadImage(data);

        // 4. 创建材质
        this.material = new Cesium.Material({
            fabric: {
                type: 'material',
                uniforms: {
                    dataTexture: dataImage,
                    colorLUT: lutTexture,
                    minValue: minValue,
                    maxValue: maxValue,
                    opacity: opacity / 100,
                    visible: 1.0
                },
                source: this.getShaderSource()
            }
        });

        // 5. 创建几何体
        const rectangle = Cesium.Rectangle.fromDegrees(
            extend[0], extend[1], extend[2], extend[3]
        );

        const geometry = new Cesium.RectangleGeometry({
            rectangle: rectangle,
            vertexFormat: Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT,
            height: 0
        });

        const instance = new Cesium.GeometryInstance({
            id: id,
            geometry: geometry
        });

        const appearance = new Cesium.EllipsoidSurfaceAppearance({
            material: this.material,
            translucent: true,
            flat: true,
            aboveGround: true
        });

        // 6. 创建Primitive(使用普通Primitive替代GroundPrimitive,避免地形细分产生的接缝)
        this.layer = new Cesium.Primitive({
            geometryInstances: instance,
            appearance: appearance,
            asynchronous: false
        });

        // 7. 添加到场景
        this.viewer.scene.primitives.add(this.layer);
        this.isVisible = true;

        return this.layer;



    }

    /**
     * 显示图层
     */
    showLayer() {
        if (this.layer) {
            this.layer.show = true
            if (this.material?.uniforms) {
                this.material.uniforms.visible = 1.0;
            }
            this.isVisible = true;
        }
    }

    /**
     * 隐藏图层
     */
    hideLayer() {
        if (this.layer) {
            this.layer.show = false;
            if (this.material?.uniforms) {
                this.material.uniforms.visible = 0.0;
            }
            this.isVisible = false;
        }
    }




    
}

export default StaticLayers;

使用方法:

let baseMap;
let baseViewer;
let staticLayers;
onMounted(() => {
  baseMap= new BaseMap('cesiumContainer');
  baseViewer = baseMap.getViewer()
  staticLayers = new StaticLayers(baseViewer);

  // 隐藏logo
  baseViewer.cesiumWidget.creditContainer.style.display = 'none';

});

const createLayer = () => {
  const extend = [spotTempData.min_lon, spotTempData.min_lat, spotTempData.max_lon, spotTempData.max_lat];

  const staticOptions = {
    ...spotTempData,
    extend,
    colorParams: {...colorList}
  }

  staticLayers.addLayer(staticOptions)
}