450 lines
14 KiB
TypeScript
450 lines
14 KiB
TypeScript
/**
|
||
* 根据经纬度范围和瓦片级别生成细分网格
|
||
* 用于globe模式下的球面渲染
|
||
*/
|
||
|
||
import { MercatorCoordinate } from 'maplibre-gl'
|
||
|
||
export interface TileMeshOptions {
|
||
/** 经纬度边界 [west, south, east, north] */
|
||
bounds: [number, number, number, number];
|
||
/** 瓦片缩放级别 */
|
||
z: number;
|
||
/** 细分级别,默认为瓦片级别的2倍 */
|
||
subdivisions?: number;
|
||
}
|
||
|
||
export interface TileMeshResult {
|
||
/** 顶点数据 (x, y, u, v) 坐标对:位置和纹理坐标 */
|
||
vertices: Float32Array;
|
||
/** 索引数据 */
|
||
indices: Uint16Array | Uint32Array;
|
||
/** 是否使用32位索引 */
|
||
uses32bitIndices: boolean;
|
||
/** 顶点数量 */
|
||
vertexCount: number;
|
||
/** 三角形数量 */
|
||
triangleCount: number;
|
||
}
|
||
|
||
/**
|
||
* 设备性能等级
|
||
*/
|
||
export enum PerformanceLevel {
|
||
LOW = 'low', // 低端设备
|
||
MEDIUM = 'medium', // 中端设备
|
||
HIGH = 'high' // 高端设备
|
||
}
|
||
|
||
/**
|
||
* 根据当前缩放级别和设备性能计算最佳细分数量
|
||
*/
|
||
export function calculateOptimalSubdivisions(
|
||
zoomLevel: number,
|
||
performanceLevel: PerformanceLevel = PerformanceLevel.MEDIUM,
|
||
options?: {
|
||
/** 最小细分数量,默认4 */
|
||
minSubdivisions?: number;
|
||
/** 最大细分数量,默认64 */
|
||
maxSubdivisions?: number;
|
||
/** 是否为Globe模式,Globe模式需要更多细分,默认true */
|
||
isGlobeMode?: boolean;
|
||
/** 视口区域大小(像素),影响所需细分程度 */
|
||
viewportSize?: { width: number; height: number };
|
||
}
|
||
): number {
|
||
const {
|
||
minSubdivisions = 4,
|
||
maxSubdivisions = 64,
|
||
isGlobeMode = true,
|
||
viewportSize
|
||
} = options || {};
|
||
|
||
// 基础细分计算:随着缩放级别指数增长
|
||
let baseSubdivisions: number;
|
||
|
||
if (zoomLevel <= 2) {
|
||
// 非常低的缩放级别,使用最少细分
|
||
baseSubdivisions = minSubdivisions;
|
||
} else if (zoomLevel <= 6) {
|
||
// 低到中等缩放级别:线性增长
|
||
baseSubdivisions = minSubdivisions + (zoomLevel - 2) * 2;
|
||
} else if (zoomLevel <= 12) {
|
||
// 中等到高缩放级别:较快增长
|
||
baseSubdivisions = 12 + (zoomLevel - 6) * 3;
|
||
} else {
|
||
// 高缩放级别:平缓增长避免性能问题
|
||
baseSubdivisions = 30 + (zoomLevel - 12) * 1.5;
|
||
}
|
||
|
||
// 设备性能调整系数
|
||
const performanceMultipliers = {
|
||
[PerformanceLevel.LOW]: 0.6, // 低端设备减少40%细分
|
||
[PerformanceLevel.MEDIUM]: 1.0, // 中端设备保持基础细分
|
||
[PerformanceLevel.HIGH]: 1.4 // 高端设备增加40%细分
|
||
};
|
||
|
||
baseSubdivisions *= performanceMultipliers[performanceLevel];
|
||
|
||
// Globe模式调整:球面渲染需要更多细分来避免失真
|
||
if (isGlobeMode) {
|
||
baseSubdivisions *= 1.2;
|
||
}
|
||
|
||
// 视口大小调整:大视口需要更多细分保证质量
|
||
if (viewportSize) {
|
||
const viewportArea = viewportSize.width * viewportSize.height;
|
||
const standardArea = 1920 * 1080; // 标准1080p面积
|
||
const sizeMultiplier = Math.sqrt(viewportArea / standardArea);
|
||
baseSubdivisions *= Math.min(1.5, Math.max(0.7, sizeMultiplier));
|
||
}
|
||
|
||
// 确保在合理范围内并且为2的幂次(对GPU更友好)
|
||
const clampedSubdivisions = Math.max(minSubdivisions, Math.min(maxSubdivisions, baseSubdivisions));
|
||
|
||
// 向最近的2的幂次取整(4, 8, 16, 32, 64等)
|
||
const powerOfTwo = Math.pow(2, Math.round(Math.log2(clampedSubdivisions)));
|
||
|
||
return Math.max(minSubdivisions, Math.min(maxSubdivisions, powerOfTwo));
|
||
}
|
||
|
||
/**
|
||
* 检测设备性能等级(简化版本)
|
||
*/
|
||
export function detectPerformanceLevel(): PerformanceLevel {
|
||
if (typeof window === 'undefined') return PerformanceLevel.HIGH;
|
||
|
||
// 检查硬件并发数
|
||
const hardwareConcurrency = navigator.hardwareConcurrency || 4;
|
||
|
||
// 检查内存信息(如果可用)
|
||
const memory = (navigator as any).deviceMemory;
|
||
|
||
// 检查WebGL能力
|
||
const canvas = document.createElement('canvas');
|
||
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
|
||
|
||
if (!gl) return PerformanceLevel.LOW;
|
||
|
||
const renderer = gl.getParameter(gl.RENDERER) || '';
|
||
const vendor = gl.getParameter(gl.VENDOR) || '';
|
||
|
||
// 基于多个指标综合判断
|
||
let score = 0;
|
||
|
||
// CPU核心数评分
|
||
if (hardwareConcurrency >= 8) score += 3;
|
||
else if (hardwareConcurrency >= 4) score += 2;
|
||
else score += 1;
|
||
|
||
// 内存评分
|
||
if (memory) {
|
||
if (memory >= 8) score += 3;
|
||
else if (memory >= 4) score += 2;
|
||
else score += 1;
|
||
} else {
|
||
score += 2; // 默认中等
|
||
}
|
||
|
||
// GPU评分(简化判断)
|
||
if (renderer.toLowerCase().includes('intel')) {
|
||
score += 1; // 集成显卡通常性能较低
|
||
} else if (renderer.toLowerCase().includes('nvidia') || renderer.toLowerCase().includes('amd')) {
|
||
score += 3; // 独立显卡性能较好
|
||
} else {
|
||
score += 2; // 默认
|
||
}
|
||
|
||
// 根据总分判断性能等级
|
||
if (score <= 4) return PerformanceLevel.LOW;
|
||
else if (score <= 7) return PerformanceLevel.MEDIUM;
|
||
else return PerformanceLevel.HIGH;
|
||
}
|
||
|
||
/**
|
||
* 将经纬度转换为Web Mercator坐标 (归一化到0-1范围)
|
||
*/
|
||
function lonLatToMercator(lon: number, lat: number): [number, number] {
|
||
const mercator = MercatorCoordinate.fromLngLat({ lng: lon, lat: lat })
|
||
return [mercator.x, mercator.y]
|
||
// const x = (lon + 180) / 360;
|
||
// const latRad = (lat * Math.PI) / 180;
|
||
// const y = (1 - Math.log(Math.tan(latRad / 2 + Math.PI / 4)) / Math.PI) / 2;
|
||
// return [x, y];
|
||
}
|
||
|
||
/**
|
||
* 创建细分的瓦片网格
|
||
*/
|
||
export function createSubdividedTileMesh(options: TileMeshOptions): TileMeshResult {
|
||
const { bounds, z, subdivisions } = options;
|
||
const [west, south, east, north] = bounds;
|
||
|
||
// 根据瓦片级别确定细分级别
|
||
// 更高的瓦片级别需要更多的细分来在globe模式下保持平滑
|
||
const subdivLevel = subdivisions ?? Math.max(8, Math.min(32, Math.pow(2, Math.max(0, z - 5))));
|
||
|
||
// 将经纬度边界转换为归一化的Web Mercator坐标
|
||
const [mercWest, mercNorth] = lonLatToMercator(west, north);
|
||
const [mercEast, mercSouth] = lonLatToMercator(east, south);
|
||
|
||
// 创建顶点网格
|
||
const verticesPerRow = subdivLevel + 1;
|
||
const verticesPerCol = subdivLevel + 1;
|
||
const totalVertices = verticesPerRow * verticesPerCol;
|
||
|
||
// 每个顶点包含4个float值:x, y, u, v (位置 + 纹理坐标)
|
||
const vertices = new Float32Array(totalVertices * 4);
|
||
|
||
// 生成顶点
|
||
for (let row = 0; row < verticesPerCol; row++) {
|
||
for (let col = 0; col < verticesPerRow; col++) {
|
||
const vertexIndex = (row * verticesPerRow + col) * 4;
|
||
|
||
// 在归一化空间中插值
|
||
const u = col / subdivLevel;
|
||
const v = row / subdivLevel;
|
||
|
||
// 计算实际的mercator坐标 (位置)
|
||
const x = mercWest + (mercEast - mercWest) * u;
|
||
const y = mercNorth + (mercSouth - mercNorth) * v;
|
||
|
||
// 设置顶点数据:位置(x, y) + 纹理坐标(u, v)
|
||
vertices[vertexIndex] = x; // 位置 x
|
||
vertices[vertexIndex + 1] = y; // 位置 y
|
||
vertices[vertexIndex + 2] = u; // 纹理坐标 u
|
||
vertices[vertexIndex + 3] = v; // 纹理坐标 v
|
||
}
|
||
}
|
||
|
||
// 创建三角形索引
|
||
const trianglesPerRow = subdivLevel;
|
||
const trianglesPerCol = subdivLevel;
|
||
const totalTriangles = trianglesPerRow * trianglesPerCol * 2;
|
||
const totalIndices = totalTriangles * 3;
|
||
|
||
// 判断是否需要32位索引
|
||
const uses32bitIndices = totalVertices > 65535;
|
||
const indices = uses32bitIndices
|
||
? new Uint32Array(totalIndices)
|
||
: new Uint16Array(totalIndices);
|
||
|
||
let indexOffset = 0;
|
||
|
||
// 生成三角形索引 (每个四边形分成两个三角形)
|
||
for (let row = 0; row < trianglesPerCol; row++) {
|
||
for (let col = 0; col < trianglesPerRow; col++) {
|
||
// 四边形的四个顶点索引
|
||
const topLeft = row * verticesPerRow + col;
|
||
const topRight = topLeft + 1;
|
||
const bottomLeft = (row + 1) * verticesPerRow + col;
|
||
const bottomRight = bottomLeft + 1;
|
||
|
||
// 第一个三角形 (左上角)
|
||
indices[indexOffset++] = topLeft;
|
||
indices[indexOffset++] = bottomLeft;
|
||
indices[indexOffset++] = topRight;
|
||
|
||
// 第二个三角形 (右下角)
|
||
indices[indexOffset++] = topRight;
|
||
indices[indexOffset++] = bottomLeft;
|
||
indices[indexOffset++] = bottomRight;
|
||
}
|
||
}
|
||
|
||
return {
|
||
vertices,
|
||
indices,
|
||
uses32bitIndices,
|
||
vertexCount: totalVertices,
|
||
triangleCount: totalTriangles
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 根据瓦片坐标和级别创建细分网格
|
||
*/
|
||
export function createSubdividedTileMeshFromTileCoords(
|
||
x: number,
|
||
y: number,
|
||
z: number,
|
||
subdivisions?: number
|
||
): TileMeshResult {
|
||
// 计算瓦片的经纬度边界
|
||
const n = Math.pow(2, z);
|
||
const west = (x / n) * 360 - 180;
|
||
const east = ((x + 1) / n) * 360 - 180;
|
||
|
||
const latRad1 = Math.atan(Math.sinh(Math.PI * (1 - 2 * y / n)));
|
||
const latRad2 = Math.atan(Math.sinh(Math.PI * (1 - 2 * (y + 1) / n)));
|
||
|
||
const north = (latRad1 * 180) / Math.PI;
|
||
const south = (latRad2 * 180) / Math.PI;
|
||
|
||
return createSubdividedTileMesh({
|
||
bounds: [west, south, east, north],
|
||
z,
|
||
subdivisions
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 创建覆盖整个世界的细分网格
|
||
*/
|
||
export function createWorldSubdividedMesh(z: number, subdivisions?: number): TileMeshResult {
|
||
return createSubdividedTileMesh({
|
||
bounds: [-180, -85.0511, 180, 85.0511], // Web Mercator的纬度范围
|
||
z,
|
||
subdivisions
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 使用示例函数:为特定地理区域创建高精度细分网格
|
||
*/
|
||
export function createRegionSubdividedMesh(
|
||
/** 中心经度 */
|
||
centerLon: number,
|
||
/** 中心纬度 */
|
||
centerLat: number,
|
||
/** 经度范围(度) */
|
||
lonRange: number,
|
||
/** 纬度范围(度) */
|
||
latRange: number,
|
||
/** 瓦片缩放级别 */
|
||
z: number,
|
||
/** 自定义细分级别 */
|
||
subdivisions?: number
|
||
): TileMeshResult {
|
||
const west = centerLon - lonRange / 2;
|
||
const east = centerLon + lonRange / 2;
|
||
const south = centerLat - latRange / 2;
|
||
const north = centerLat + latRange / 2;
|
||
|
||
return createSubdividedTileMesh({
|
||
bounds: [west, south, east, north],
|
||
z,
|
||
subdivisions
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 预设区域网格创建器
|
||
*/
|
||
export const RegionMeshPresets = {
|
||
/** 新加坡区域 */
|
||
singapore: (z: number, subdivisions?: number) => createSubdividedTileMesh({
|
||
bounds: [103.6, 1.2, 104.0, 1.5],
|
||
z,
|
||
subdivisions
|
||
}),
|
||
|
||
/** 中国大陆区域 */
|
||
china: (z: number, subdivisions?: number) => createSubdividedTileMesh({
|
||
bounds: [65.24686921730095, 11.90274236858339, 138.85323419021077, 55.34323805611308],
|
||
z,
|
||
subdivisions
|
||
}),
|
||
|
||
/** 美国本土区域 */
|
||
usa: (z: number, subdivisions?: number) => createSubdividedTileMesh({
|
||
bounds: [-125, 25, -66, 49],
|
||
z,
|
||
subdivisions
|
||
}),
|
||
|
||
/** 欧洲区域 */
|
||
europe: (z: number, subdivisions?: number) => createSubdividedTileMesh({
|
||
bounds: [-10, 35, 40, 70],
|
||
z,
|
||
subdivisions
|
||
})
|
||
};
|
||
|
||
/**
|
||
* 便捷函数:自动计算最佳细分并创建世界网格
|
||
*/
|
||
export function createOptimalWorldMesh(
|
||
zoomLevel: number,
|
||
performanceLevel?: PerformanceLevel,
|
||
options?: {
|
||
viewportSize?: { width: number; height: number };
|
||
isGlobeMode?: boolean;
|
||
minSubdivisions?: number;
|
||
maxSubdivisions?: number;
|
||
}
|
||
): TileMeshResult {
|
||
const detectedPerformance = performanceLevel ?? detectPerformanceLevel();
|
||
const optimalSubdivisions = calculateOptimalSubdivisions(
|
||
zoomLevel,
|
||
detectedPerformance,
|
||
options
|
||
);
|
||
|
||
return createWorldSubdividedMesh(zoomLevel, optimalSubdivisions);
|
||
}
|
||
|
||
/**
|
||
* 便捷函数:自动计算最佳细分并创建指定区域网格
|
||
*/
|
||
export function createOptimalRegionMesh(
|
||
bounds: [number, number, number, number],
|
||
zoomLevel: number,
|
||
performanceLevel?: PerformanceLevel,
|
||
options?: {
|
||
viewportSize?: { width: number; height: number };
|
||
isGlobeMode?: boolean;
|
||
minSubdivisions?: number;
|
||
maxSubdivisions?: number;
|
||
}
|
||
): TileMeshResult {
|
||
const detectedPerformance = performanceLevel ?? detectPerformanceLevel();
|
||
const optimalSubdivisions = calculateOptimalSubdivisions(
|
||
zoomLevel,
|
||
detectedPerformance,
|
||
options
|
||
);
|
||
|
||
return createSubdividedTileMesh({
|
||
bounds,
|
||
z: zoomLevel,
|
||
subdivisions: optimalSubdivisions
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 获取细分级别建议的可读描述
|
||
*/
|
||
export function getSubdivisionRecommendation(zoomLevel: number, performanceLevel: PerformanceLevel): {
|
||
subdivisions: number;
|
||
description: string;
|
||
triangleCount: number;
|
||
estimatedMemoryMB: number;
|
||
} {
|
||
const subdivisions = calculateOptimalSubdivisions(zoomLevel, performanceLevel);
|
||
const triangleCount = subdivisions * subdivisions * 2;
|
||
const vertexCount = (subdivisions + 1) * (subdivisions + 1);
|
||
|
||
// 估算内存使用 (顶点 + 索引)
|
||
const vertexMemory = vertexCount * 4 * 4; // 4个float32 per vertex
|
||
const indexMemory = triangleCount * 3 * (vertexCount > 65535 ? 4 : 2); // 3 indices per triangle
|
||
const estimatedMemoryMB = (vertexMemory + indexMemory) / (1024 * 1024);
|
||
|
||
let description: string;
|
||
if (subdivisions <= 8) {
|
||
description = "低细分 - 高性能,适合远距离视图";
|
||
} else if (subdivisions <= 16) {
|
||
description = "中等细分 - 平衡性能和质量";
|
||
} else if (subdivisions <= 32) {
|
||
description = "高细分 - 高质量,适合近距离视图";
|
||
} else {
|
||
description = "超高细分 - 最高质量,需要高端设备";
|
||
}
|
||
|
||
return {
|
||
subdivisions,
|
||
description,
|
||
triangleCount,
|
||
estimatedMemoryMB: Math.round(estimatedMemoryMB * 100) / 100
|
||
};
|
||
}
|