Cesium 地球网格构造
Cesium原理篇:3最长的一帧之地形(2:高度图)
HeightmapTessellator
用于从高程图像创建网格。提供了一个函数 computeVertices
,可以根据高程图像创建顶点数组。
该函数的参数包括高程图像、高度数据的结构、网格宽高、边缘裙板高度、矩形范围、相机中心点等。函数的实现使用了许多性能优化技巧,如将函数内常量化、内联等。
该模块的输出为一个对象,包括创建好的顶点数组、最大与最小高度、该网格的边界球、边界方向盒等信息。
1、函数定义
// 声明
static computeVertices(options) {}
// 使用
const width = 5;
const height = 5;
const statistics = Cesium.HeightmapTessellator.computeVertices({
heightmap : [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
width : width,
height : height,
skirtHeight : 0.0,
nativeRectangle : {
west : 10.0,
east : 20.0,
south : 30.0,
north : 40.0
}
});
const encoding = statistics.encoding;
const position = encoding.decodePosition(statistics.vertices, index);
options
参数:
参数 | 类型 | 描述 |
---|---|---|
heightmap | Array | 要镶嵌的高度图。 |
width | number | 高度图的宽度(以高度样本计)。 |
height | number | 高度图的高度(以高度样本计)。 |
skirtHeight | number | 要悬垂在高度图边缘的裙子的高度。 |
nativeRectangle | Rectangle | 高度贴图投影的原始坐标中的矩形。对于具有地理投影的高度图,这是度数。对于 Web 墨卡托投影,这是米。 |
exaggeration | number | 用于夸大地形的比例尺。默认为1. |
exaggerationRelativeHeight | number | 地形夸大的高度,以米为单位。默认为0. |
rectangle | Rectangle | 高度图覆盖的矩形,大地坐标为北、南、东和西属性(弧度)。必须提供矩形或本机矩形。如果同时提供两者,则假定它们是一致的。 |
isGeographic | boolean | 如果高度图使用{@link GeographicProjection},则为true;如果使用{@link WebMercatorProjection},则为false。默认为true。 |
relativeToCenter | Cartesian3 | 将计算出的位置作为Cartesian3.subtract(worldPosition, relativeToCenter) 。默认为Cartesian3.ZERO。 |
ellipsoid | Ellipsoid | 高度贴图适用的椭球体。默认为Ellipsoid.WGS84。 |
structure | object | 描述高度数据结构的对象。 |
裙边(skirt)
防止不同层级mesh加载时出现裂缝
实现
static computeVertices(options) {
const cos = Math.cos;
const sin = Math.sin;
const sqrt = Math.sqrt;
const toRadians = CesiumMath.toRadians;
const heightmap = options.heightmap;
const width = options.width;
const height = options.height;
const skirtHeight = options.skirtHeight;
const hasSkirts = skirtHeight > 0.0;
const ellipsoid = defaultValue(options.ellipsoid, Ellipsoid.WGS84);
const nativeRectangle = Rectangle.clone(options.nativeRectangle);
const rectangle = Rectangle.clone(options.rectangle);
const geographicWest = rectangle.west;
const geographicSouth = rectangle.south;
const geographicEast = rectangle.east;
const geographicNorth = rectangle.north;
const relativeToCenter = options.relativeToCenter;
const includeWebMercatorT = defaultValue(options.includeWebMercatorT, false);
const exaggeration = defaultValue(options.exaggeration, 1.0);
const exaggerationRelativeHeight = defaultValue(
options.exaggerationRelativeHeight,
0.0
);
const hasExaggeration = exaggeration !== 1.0;
const includeGeodeticSurfaceNormals = hasExaggeration;
const rectangleWidth = Rectangle.computeWidth(nativeRectangle);
const rectangleHeight = Rectangle.computeHeight(nativeRectangle);
// 每个网格的长宽
const granularityX = rectangleWidth / (width - 1);
const granularityY = rectangleHeight / (height - 1);
const radiiSquared = ellipsoid.radiiSquared;
const radiiSquaredX = radiiSquared.x;
const radiiSquaredY = radiiSquared.y;
const radiiSquaredZ = radiiSquared.z;
let minimumHeight = 65536.0;
let maximumHeight = -65536.0;
const fromENU = Transforms.eastNorthUpToFixedFrame(
relativeToCenter,
ellipsoid
);
const minimum = minimumScratch;
minimum.x = Number.POSITIVE_INFINITY;
minimum.y = Number.POSITIVE_INFINITY;
minimum.z = Number.POSITIVE_INFINITY;
const maximum = maximumScratch;
maximum.x = Number.NEGATIVE_INFINITY;
maximum.y = Number.NEGATIVE_INFINITY;
maximum.z = Number.NEGATIVE_INFINITY;
let hMin = Number.POSITIVE_INFINITY;
const gridVertexCount = width * height;
const edgeVertexCount = skirtHeight > 0.0 ? width * 2 + height * 2 : 0;
const vertexCount = gridVertexCount + edgeVertexCount;
const positions = new Array(vertexCount);
const heights = new Array(vertexCount);
const uvs = new Array(vertexCount);
let startRow = 0;
let endRow = height;
let startCol = 0;
let endCol = width;
if (hasSkirts) {
--startRow;
++endRow;
--startCol;
++endCol;
}
for (let rowIndex = startRow; rowIndex < endRow; ++rowIndex) {
let row = rowIndex;
if (row < 0) {
row = 0;
}
if (row >= height) {
row = height - 1;
}
//
// ^ latitude(纬度)
// |
// | North(90)
// | ---------
// | | |
// | West(-180) | | East(0)
// | | |
// | ---------
// | South(-90)
// -----------------------------> longitude(经度)
// 地理坐标系下
// 当前纬度(latitude) 距离最北头(North) 的距离
// 这个值是越来越小的, 随着行数越来越大
let latitude = nativeRectangle.north - granularityY * row;
latitude = toRadians(latitude);
// 当前纬度(latitude) 距离最南头(South) 的百分比(0~1)
let v = (latitude - geographicSouth) / (geographicNorth - geographicSouth);
v = CesiumMath.clamp(v, 0.0, 1.0);
const isNorthEdge = rowIndex === startRow;
const isSouthEdge = rowIndex === endRow - 1;
const cosLatitude = cos(latitude);
const nZ = sin(latitude);
const kZ = radiiSquaredZ * nZ;
for (let colIndex = startCol; colIndex < endCol; ++colIndex) {
let col = colIndex;
if (col < 0) {
col = 0;
}
if (col >= width) {
col = width - 1;
}
const terrainOffset = row * width + col;
let heightSample = heightmap[terrainOffset]
let longitude = nativeRectangle.west + granularityX * col;
longitude = toRadians(longitude);
let u = (longitude - geographicWest) / (geographicEast - geographicWest);
u = CesiumMath.clamp(u, 0.0, 1.0);
let index = row * width + col;
if (skirtHeight > 0.0) {
const isWestEdge = colIndex === startCol;
const isEastEdge = colIndex === endCol - 1;
const isEdge = isNorthEdge || isSouthEdge || isWestEdge || isEastEdge;
const isCorner = (isNorthEdge || isSouthEdge) && (isWestEdge || isEastEdge);
if (isCorner) {
// Don't generate skirts on the corners.
continue;
} else if (isEdge) {
heightSample -= skirtHeight;
if (isWestEdge) {
// The outer loop iterates north to south but the indices are ordered south to north, hence the index flip below
// 外循环从北到南迭代,但索引按从南到北的顺序排列,因此索引在下面翻转
index = gridVertexCount + (height - row - 1);
} else if (isSouthEdge) {
// Add after west indices. South indices are ordered east to west.
// 加在西方指数之后。南方指数是从东向西排列的。
index = gridVertexCount + height + (width - col - 1);
} else if (isEastEdge) {
// Add after west and south indices. East indices are ordered north to south. The index is flipped like above.
// 在西部和南部指数后加上。东部指数是从北向南排列的。索引如上所述翻转。
index = gridVertexCount + height + width + row;
} else if (isNorthEdge) {
// Add after west, south, and east indices. North indices are ordered west to east.
// 在西部、南部和东部指数后添加。北方指数是从西向东排列的。
index = gridVertexCount + height + width + height + col;
}
}
}
// 经纬度转笛卡尔坐标系
const nX = cosLatitude * cos(longitude);
const nY = cosLatitude * sin(longitude);
const kX = radiiSquaredX * nX;
const kY = radiiSquaredY * nY;
const gamma = sqrt(kX * nX + kY * nY + kZ * nZ);
const oneOverGamma = 1.0 / gamma;
const rSurfaceX = kX * oneOverGamma;
const rSurfaceY = kY * oneOverGamma;
const rSurfaceZ = kZ * oneOverGamma;
const position = new Cartesian3();
position.x = rSurfaceX + nX * heightSample;
position.y = rSurfaceY + nY * heightSample;
position.z = rSurfaceZ + nZ * heightSample;
hMin = Math.min(hMin, heightSample);
positions[index] = position;
uvs[index] = new Cartesian2(u, v);
heights[index] = heightSample;
}
}
const aaBox = new AxisAlignedBoundingBox(minimum, maximum, relativeToCenter);
const encoding = new TerrainEncoding(
relativeToCenter,
aaBox,
hMin,
maximumHeight,
fromENU,
false,
includeWebMercatorT,
includeGeodeticSurfaceNormals,
exaggeration,
exaggerationRelativeHeight
);
const vertices = new Float32Array(vertexCount * encoding.stride);
let bufferIndex = 0;
for (let j = 0; j < vertexCount; ++j) {
bufferIndex = encoding.encode(
vertices,
bufferIndex,
positions[j],
uvs[j],
heights[j],
undefined,
undefined,
undefined
);
}
return {
vertices: vertices,
};
}