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;
}
输出为
(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));
在着色器中使用最邻近采样(另一篇文章有解释最邻近采样和线性插值,有兴趣的可以看一下)
/**
* 获取着色器源码
* @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的区别
3、最终实现效果:
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)
}