radar layer
This commit is contained in:
parent
33af6ca468
commit
8e847d44a8
@ -25,6 +25,8 @@ import {
|
||||
} from "lucide-react"
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTimeline } from '@/hooks/use-timeline';
|
||||
import { useEffect } from 'react'
|
||||
import { useRadarTile } from '@/hooks/use-radartile'
|
||||
|
||||
|
||||
export default function Page() {
|
||||
@ -42,6 +44,11 @@ export default function Page() {
|
||||
const startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // 7天前
|
||||
const endDate = new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000); // 3天后
|
||||
const { setTime } = useTimeline()
|
||||
const { fetchRadarTile } = useRadarTile({})
|
||||
|
||||
useEffect(() => {
|
||||
fetchRadarTile("http://127.0.0.1:3050/test")
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
|
||||
251
app/timeline.tsx
251
app/timeline.tsx
@ -707,95 +707,7 @@ export const Timeline: React.FC<Props> = ({
|
||||
}
|
||||
|
||||
|
||||
const gl = (canvasRef.current.getContext('webgl2', {
|
||||
antialias: true, // 启用抗锯齿
|
||||
alpha: true, // 启用alpha通道以支持透明度
|
||||
premultipliedAlpha: false, // 不使用预乘alpha
|
||||
depth: false, // 不需要深度缓冲
|
||||
stencil: false, // 不需要模板缓冲
|
||||
preserveDrawingBuffer: false // 不保留绘制缓冲区
|
||||
}) as WebGL2RenderingContext);
|
||||
if (!gl) {
|
||||
console.error('WebGL2 not supported');
|
||||
return;
|
||||
}
|
||||
|
||||
const program = createProgram(gl);
|
||||
if (!program) {
|
||||
console.error('Failed to create program');
|
||||
return;
|
||||
}
|
||||
|
||||
// 绑定uniform buffer到着色器程序
|
||||
const uniformBlockIndex = gl.getUniformBlockIndex(program, 'Uniforms');
|
||||
if (uniformBlockIndex !== gl.INVALID_INDEX) {
|
||||
gl.uniformBlockBinding(program, uniformBlockIndex, 0);
|
||||
}
|
||||
|
||||
const vao = gl.createVertexArray();
|
||||
|
||||
if (!vao) {
|
||||
console.error('Failed to create vertex array');
|
||||
return;
|
||||
}
|
||||
gl.bindVertexArray(vao);
|
||||
|
||||
const vertex_bf = defaultVb(gl);
|
||||
const { buffer: instants_bf, count: actualInstanceCount } = createVesicaInstances(gl, actualVesicaData, actualWidth, actualHeight, dpr);
|
||||
const uniform_bf = defaultUb(gl, current_uniforms.current);
|
||||
|
||||
gl.bindVertexArray(null);
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
||||
gl.bindBuffer(gl.UNIFORM_BUFFER, null);
|
||||
|
||||
vaoRef.current = vao;
|
||||
vertex_bfRef.current = vertex_bf;
|
||||
uniform_bfRef.current = uniform_bf;
|
||||
instants_bfRef.current = instants_bf;
|
||||
programRef.current = program;
|
||||
instants_countRef.current = actualInstanceCount; // 使用实际生成的实例数量
|
||||
|
||||
function render() {
|
||||
gl.clearColor(0, 0, 0, 0); // 深灰背景,便于看到刻度
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
gl.useProgram(program);
|
||||
gl.bindVertexArray(vaoRef.current);
|
||||
|
||||
// 绑定uniform buffer
|
||||
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, uniform_bfRef.current);
|
||||
|
||||
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, instants_countRef.current);
|
||||
gl.bindVertexArray(null);
|
||||
}
|
||||
|
||||
function updateUniforms(uniforms: Uniforms) {
|
||||
gl.bindBuffer(gl.UNIFORM_BUFFER, uniform_bfRef.current!);
|
||||
const uniformData = new Float32Array([
|
||||
uniforms.startTimestamp,
|
||||
uniforms.endTimestamp,
|
||||
uniforms.currentTimestamp,
|
||||
uniforms.radius,
|
||||
uniforms.d,
|
||||
uniforms.timelineStartX,
|
||||
uniforms.timelineEndX,
|
||||
0.0, // padding - 填充以对齐到8字节边界
|
||||
uniforms.viewportSize[0],
|
||||
uniforms.viewportSize[1],
|
||||
uniforms.zoomLevel,
|
||||
uniforms.panOffset
|
||||
]);
|
||||
gl.bufferData(gl.UNIFORM_BUFFER, uniformData, gl.DYNAMIC_DRAW);
|
||||
}
|
||||
|
||||
|
||||
// TODO: 可以通过props传入自定义的时间轴刻度数据
|
||||
// 或使用useImperativeHandle暴露更新方法
|
||||
|
||||
// 初始化uniform数据并渲染
|
||||
updateUniforms(current_uniforms.current);
|
||||
gl.viewport(0, 0, actualWidth, actualHeight);
|
||||
render();
|
||||
|
||||
// 鼠标滚轮缩放和平移
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
@ -899,56 +811,7 @@ export const Timeline: React.FC<Props> = ({
|
||||
// 添加全局鼠标抬起事件,防止鼠标移出canvas后拖拽卡住
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
// 使用ResizeObserver监听canvas尺寸变化
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const { width: new_width, height: new_height } = entry.contentRect;
|
||||
|
||||
// 获取设备像素比例以支持高分屏
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const actualWidth = Math.floor(new_width * dpr);
|
||||
const actualHeight = Math.floor(new_height * dpr);
|
||||
|
||||
// 更新canvas的实际像素尺寸
|
||||
canvasRef.current!.width = actualWidth;
|
||||
canvasRef.current!.height = actualHeight;
|
||||
|
||||
// 设置CSS显示尺寸
|
||||
canvasRef.current!.style.width = new_width + 'px';
|
||||
canvasRef.current!.style.height = new_height + 'px';
|
||||
|
||||
// 更新刻度线canvas的尺寸
|
||||
setupTicksCanvas(ticksCanvasRef.current!, new_width, new_height, dpr);
|
||||
|
||||
// ResizeObserver中需要立即重绘,因为setupTicksCanvas会清空canvas
|
||||
redraw();
|
||||
|
||||
// 更新uniform数据
|
||||
current_uniforms.current.viewportSize = [actualWidth, actualHeight];
|
||||
current_uniforms.current.radius = radius * dpr; // 调整radius以适应像素密度
|
||||
current_uniforms.current.d = d * dpr; // 调整d以适应像素密度
|
||||
current_uniforms.current.timelineStartX = 40 * dpr; // 时间轴开始坐标
|
||||
current_uniforms.current.timelineEndX = (new_width - 40) * dpr; // 时间轴结束坐标
|
||||
current_uniforms.current.zoomLevel = stateRef.current.zoomLevel;
|
||||
current_uniforms.current.panOffset = stateRef.current.panOffset * dpr;
|
||||
|
||||
// 重新生成实例数据以适应新的canvas尺寸
|
||||
const { buffer: new_instants_bf, count: new_count } = createVesicaInstances(gl, actualVesicaData, actualWidth, actualHeight, dpr);
|
||||
|
||||
// 更新实例buffer引用和数量
|
||||
if (instants_bfRef.current) {
|
||||
gl.deleteBuffer(instants_bfRef.current);
|
||||
}
|
||||
instants_bfRef.current = new_instants_bf;
|
||||
instants_countRef.current = new_count;
|
||||
|
||||
updateUniforms(current_uniforms.current);
|
||||
gl.viewport(0, 0, actualWidth, actualHeight);
|
||||
render();
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(canvasRef.current!);
|
||||
|
||||
return () => {
|
||||
// 移除事件监听器
|
||||
@ -980,14 +843,7 @@ export const Timeline: React.FC<Props> = ({
|
||||
// 重置临时状态(不需要调用setState,因为组件即将卸载或重新初始化)
|
||||
panTempOffsetRef.current = 0;
|
||||
|
||||
resizeObserver.disconnect();
|
||||
|
||||
// 清理WebGL资源
|
||||
if (vaoRef.current) gl.deleteVertexArray(vaoRef.current);
|
||||
if (vertex_bfRef.current) gl.deleteBuffer(vertex_bfRef.current);
|
||||
if (uniform_bfRef.current) gl.deleteBuffer(uniform_bfRef.current);
|
||||
if (instants_bfRef.current) gl.deleteBuffer(instants_bfRef.current);
|
||||
if (programRef.current) gl.deleteProgram(programRef.current);
|
||||
}
|
||||
}, [boxSize, actualVesicaData]);
|
||||
|
||||
@ -1043,8 +899,115 @@ export const Timeline: React.FC<Props> = ({
|
||||
gl.bindVertexArray(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}, [state.zoomLevel, state.panOffset, startDate, endDate, currentDate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canvasRef.current) return;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const displayWidth = canvasRef.current.clientWidth;
|
||||
const displayHeight = canvasRef.current.clientHeight;
|
||||
|
||||
// 设置WebGL canvas的实际像素尺寸(考虑高分屏)
|
||||
const actualWidth = Math.floor(displayWidth * dpr);
|
||||
const actualHeight = Math.floor(displayHeight * dpr);
|
||||
|
||||
canvasRef.current.width = actualWidth;
|
||||
canvasRef.current.height = actualHeight;
|
||||
const gl = (canvasRef.current.getContext('webgl2', {
|
||||
antialias: true, // 启用抗锯齿
|
||||
alpha: true, // 启用alpha通道以支持透明度
|
||||
premultipliedAlpha: false, // 不使用预乘alpha
|
||||
depth: false, // 不需要深度缓冲
|
||||
stencil: false, // 不需要模板缓冲
|
||||
preserveDrawingBuffer: false // 不保留绘制缓冲区
|
||||
}) as WebGL2RenderingContext);
|
||||
if (!gl) {
|
||||
console.error('WebGL2 not supported');
|
||||
return;
|
||||
}
|
||||
|
||||
const program = createProgram(gl);
|
||||
if (!program) {
|
||||
console.error('Failed to create program');
|
||||
return;
|
||||
}
|
||||
|
||||
// 绑定uniform buffer到着色器程序
|
||||
const uniformBlockIndex = gl.getUniformBlockIndex(program, 'Uniforms');
|
||||
if (uniformBlockIndex !== gl.INVALID_INDEX) {
|
||||
gl.uniformBlockBinding(program, uniformBlockIndex, 0);
|
||||
}
|
||||
|
||||
const vao = gl.createVertexArray();
|
||||
|
||||
if (!vao) {
|
||||
console.error('Failed to create vertex array');
|
||||
return;
|
||||
}
|
||||
gl.bindVertexArray(vao);
|
||||
|
||||
const vertex_bf = defaultVb(gl);
|
||||
const { buffer: instants_bf, count: actualInstanceCount } = createVesicaInstances(gl, actualVesicaData, actualWidth, actualHeight, dpr);
|
||||
const uniform_bf = defaultUb(gl, current_uniforms.current);
|
||||
|
||||
gl.bindVertexArray(null);
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
||||
gl.bindBuffer(gl.UNIFORM_BUFFER, null);
|
||||
|
||||
vaoRef.current = vao;
|
||||
vertex_bfRef.current = vertex_bf;
|
||||
uniform_bfRef.current = uniform_bf;
|
||||
instants_bfRef.current = instants_bf;
|
||||
programRef.current = program;
|
||||
instants_countRef.current = actualInstanceCount; // 使用实际生成的实例数量
|
||||
|
||||
function render() {
|
||||
gl.clearColor(0, 0, 0, 0); // 深灰背景,便于看到刻度
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
gl.useProgram(program);
|
||||
gl.bindVertexArray(vaoRef.current);
|
||||
|
||||
// 绑定uniform buffer
|
||||
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, uniform_bfRef.current);
|
||||
|
||||
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, instants_countRef.current);
|
||||
gl.bindVertexArray(null);
|
||||
}
|
||||
|
||||
function updateUniforms(uniforms: Uniforms) {
|
||||
gl.bindBuffer(gl.UNIFORM_BUFFER, uniform_bfRef.current!);
|
||||
const uniformData = new Float32Array([
|
||||
uniforms.startTimestamp,
|
||||
uniforms.endTimestamp,
|
||||
uniforms.currentTimestamp,
|
||||
uniforms.radius,
|
||||
uniforms.d,
|
||||
uniforms.timelineStartX,
|
||||
uniforms.timelineEndX,
|
||||
0.0, // padding - 填充以对齐到8字节边界
|
||||
uniforms.viewportSize[0],
|
||||
uniforms.viewportSize[1],
|
||||
uniforms.zoomLevel,
|
||||
uniforms.panOffset
|
||||
]);
|
||||
gl.bufferData(gl.UNIFORM_BUFFER, uniformData, gl.DYNAMIC_DRAW);
|
||||
}
|
||||
|
||||
|
||||
// TODO: 可以通过props传入自定义的时间轴刻度数据
|
||||
// 或使用useImperativeHandle暴露更新方法
|
||||
|
||||
// 初始化uniform数据并渲染
|
||||
updateUniforms(current_uniforms.current);
|
||||
|
||||
gl.viewport(0, 0, actualWidth, actualHeight);
|
||||
render();
|
||||
}, [canvasRef.current]);
|
||||
|
||||
return (
|
||||
<div className={cn(props.className, "w-full h-12 flex flex-row")}>
|
||||
<div className="h-full flex flex-row items-center px-3 gap-2" style={{ boxShadow: '8px 0 24px rgba(0, 0, 0, 0.15), 4px 0 12px rgba(0, 0, 0, 0.1)' }}>
|
||||
|
||||
@ -1,20 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import maplibregl, { ProjectionDefinition } from 'maplibre-gl'
|
||||
import maplibregl, { CustomLayerInterface, CustomRenderMethodInput, createTileMesh, Projection } from 'maplibre-gl'
|
||||
import 'maplibre-gl/dist/maplibre-gl.css'
|
||||
import { useMap } from '@/app/map-context'
|
||||
import { apply, applyStyle } from 'ol-mapbox-style';
|
||||
import { useTheme } from '@/components/theme-provider'
|
||||
// import TileWMS from 'ol/source/TileWMS.js';
|
||||
// import Map from 'ol/Map';
|
||||
// import View from 'ol/View';
|
||||
// import TileLayer from 'ol/layer/Tile';
|
||||
// import { transformExtent, fromLonLat } from 'ol/proj.js';
|
||||
// import StadiaMaps from 'ol/source/StadiaMaps.js';
|
||||
// import XYZ from 'ol/source/XYZ';
|
||||
// import 'ol/ol.css';
|
||||
import { useMapLocation } from '@/hooks/use-map-location'
|
||||
import { createOptimalWorldMesh, getSubdivisionRecommendation, detectPerformanceLevel, createOptimalRegionMesh, RegionMeshPresets } from '@/lib/tile-mesh'
|
||||
import { useRadarTile } from '@/hooks/use-radartile'
|
||||
|
||||
interface MapComponentProps {
|
||||
style?: string
|
||||
@ -32,6 +24,10 @@ export function MapComponent({
|
||||
const mapContainer = useRef<HTMLDivElement>(null)
|
||||
const { setMap } = useMap()
|
||||
const { location } = useMapLocation()
|
||||
const { radarTileRef } = useRadarTile()
|
||||
const texRef = useRef<WebGLTexture | null>(null)
|
||||
const lutTexRef = useRef<WebGLTexture | null>(null)
|
||||
const glRef = useRef<WebGL2RenderingContext | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapContainer.current) return
|
||||
@ -79,21 +75,343 @@ export function MapComponent({
|
||||
type: 'globe'
|
||||
})
|
||||
|
||||
map.addSource('nexrad', {
|
||||
type: 'raster',
|
||||
tiles: [
|
||||
// 'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi?service=WMS&version=1.3.0&request=GetMap&layers=nexrad-n0r-wmst&styles=&format=image/png&transparent=true&crs=EPSG:3857&bbox={bbox-epsg-3857}&width=256&height=256'
|
||||
'http://127.0.0.1:3050/tiles/{z}/{x}/{y}?time=202507220012'
|
||||
],
|
||||
tileSize: 256
|
||||
});
|
||||
// map.addSource('nexrad', {
|
||||
// type: 'raster',
|
||||
// tiles: [
|
||||
// // 'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi?service=WMS&version=1.3.0&request=GetMap&layers=nexrad-n0r-wmst&styles=&format=image/png&transparent=true&crs=EPSG:3857&bbox={bbox-epsg-3857}&width=256&height=256'
|
||||
// 'http://127.0.0.1:3050/tiles/{z}/{x}/{y}?time=202507220012'
|
||||
// ],
|
||||
// tileSize: 256
|
||||
// });
|
||||
|
||||
map.addLayer({
|
||||
id: 'nexrad-layer',
|
||||
type: 'raster',
|
||||
source: 'nexrad',
|
||||
paint: { 'raster-opacity': 0.8 }
|
||||
});
|
||||
const customGlLayer: CustomGlLayer = {
|
||||
id: 'player',
|
||||
type: 'custom',
|
||||
lastZoom: -1, // 添加缓存的缩放级别
|
||||
uniformLocations: {} as Record<string, WebGLUniformLocation | null>, // 缓存uniform位置
|
||||
|
||||
prerender(gl: WebGLRenderingContext | WebGL2RenderingContext, { shaderData }: CustomRenderMethodInput) {
|
||||
|
||||
if (!this.program) {
|
||||
glRef.current = gl as WebGL2RenderingContext;
|
||||
if (!(gl instanceof WebGL2RenderingContext)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const vertexSource = `#version 300 es
|
||||
|
||||
layout(location = 0) in vec2 a_pos;
|
||||
layout(location = 1) in vec2 a_tex_coord;
|
||||
|
||||
${shaderData.vertexShaderPrelude}
|
||||
${shaderData.define}
|
||||
|
||||
out vec2 v_tex_coord;
|
||||
|
||||
void main() {
|
||||
gl_Position = projectTile(a_pos);
|
||||
v_tex_coord = a_tex_coord;
|
||||
}`;
|
||||
|
||||
// WebGL2 fragment shader
|
||||
const fragmentSource = `#version 300 es
|
||||
precision highp float;
|
||||
uniform sampler2D u_tex;
|
||||
uniform sampler2D u_lut;
|
||||
|
||||
out vec4 fragColor;
|
||||
in vec2 v_tex_coord;
|
||||
|
||||
void main() {
|
||||
float value = texture(u_tex, v_tex_coord).r;
|
||||
vec4 lutColor = texture(u_lut, vec2(value, 0.5));
|
||||
fragColor = lutColor;
|
||||
}`
|
||||
|
||||
console.log(vertexSource, fragmentSource)
|
||||
|
||||
// Helper function to compile shader
|
||||
const compileShader = (source: string, type: number): WebGLShader | null => {
|
||||
const shader = gl.createShader(type);
|
||||
if (!shader) return null;
|
||||
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
|
||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||
console.error('Shader compile error:', gl.getShaderInfoLog(shader));
|
||||
gl.deleteShader(shader);
|
||||
return null;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
// Compile shaders
|
||||
const vertexShader = compileShader(vertexSource, gl.VERTEX_SHADER);
|
||||
const fragmentShader = compileShader(fragmentSource, gl.FRAGMENT_SHADER);
|
||||
|
||||
if (!vertexShader || !fragmentShader) return;
|
||||
|
||||
// Create and link program
|
||||
const program = gl.createProgram();
|
||||
if (!program) return;
|
||||
|
||||
gl.attachShader(program, vertexShader);
|
||||
gl.attachShader(program, fragmentShader);
|
||||
gl.linkProgram(program);
|
||||
|
||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||||
console.error('Program link error:', gl.getProgramInfoLog(program));
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up shaders (they're now part of the program)
|
||||
gl.deleteShader(vertexShader);
|
||||
gl.deleteShader(fragmentShader);
|
||||
|
||||
this.program = program;
|
||||
|
||||
const tex = gl.createTexture()
|
||||
|
||||
if (!tex) {
|
||||
console.error('Failed to create texture');
|
||||
return;
|
||||
}
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||
|
||||
this.tex = tex;
|
||||
texRef.current = tex;
|
||||
|
||||
// 创建 LUT 纹理
|
||||
const lutTex = createLutTexture(gl);
|
||||
if (!lutTex) return;
|
||||
|
||||
this.lutTex = lutTex;
|
||||
lutTexRef.current = lutTex;
|
||||
|
||||
// 缓存uniform位置
|
||||
this.uniformLocations = {
|
||||
'u_projection_fallback_matrix': gl.getUniformLocation(program, 'u_projection_fallback_matrix'),
|
||||
'u_projection_matrix': gl.getUniformLocation(program, 'u_projection_matrix'),
|
||||
'u_projection_tile_mercator_coords': gl.getUniformLocation(program, 'u_projection_tile_mercator_coords'),
|
||||
'u_projection_clipping_plane': gl.getUniformLocation(program, 'u_projection_clipping_plane'),
|
||||
'u_projection_transition': gl.getUniformLocation(program, 'u_projection_transition'),
|
||||
'u_tex': gl.getUniformLocation(program, 'u_tex'),
|
||||
'u_lut': gl.getUniformLocation(program, 'u_lut')
|
||||
};
|
||||
|
||||
// 创建并绑定顶点缓冲区
|
||||
const vertexBuffer = gl.createBuffer();
|
||||
if (!vertexBuffer) {
|
||||
console.error('Failed to create vertex buffer');
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建并绑定索引缓冲区
|
||||
const indexBuffer = gl.createBuffer();
|
||||
if (!indexBuffer) {
|
||||
console.error('Failed to create index buffer');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create vertex array object (WebGL2 feature)
|
||||
const vao = gl.createVertexArray();
|
||||
if (!vao) {
|
||||
console.error('Failed to create VAO');
|
||||
return;
|
||||
}
|
||||
|
||||
gl.bindVertexArray(vao);
|
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
|
||||
|
||||
// 设置位置属性 (location = 0)
|
||||
gl.enableVertexAttribArray(0);
|
||||
gl.vertexAttribPointer(
|
||||
0, // attribute location
|
||||
2, // size (x, y)
|
||||
gl.FLOAT, // type
|
||||
false, // normalized
|
||||
16, // stride (4 floats * 4 bytes = 16 bytes per vertex)
|
||||
0 // offset (位置在开始)
|
||||
);
|
||||
gl.vertexAttribDivisor(0, 0);
|
||||
|
||||
// 设置纹理坐标属性 (location = 1)
|
||||
gl.enableVertexAttribArray(1);
|
||||
gl.vertexAttribPointer(
|
||||
1, // attribute location
|
||||
2, // size (u, v)
|
||||
gl.FLOAT, // type
|
||||
false, // normalized
|
||||
16, // stride (4 floats * 4 bytes = 16 bytes per vertex)
|
||||
8 // offset (纹理坐标在位置之后,2 floats * 4 bytes = 8 bytes)
|
||||
);
|
||||
gl.vertexAttribDivisor(1, 0);
|
||||
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
||||
|
||||
// Unbind VAO
|
||||
gl.bindVertexArray(null);
|
||||
|
||||
this.vao = vao;
|
||||
this.vertexBuffer = vertexBuffer;
|
||||
this.indexBuffer = indexBuffer;
|
||||
}
|
||||
|
||||
// 只在缩放级别变化时更新网格数据
|
||||
const currentZoom = Math.floor(map.getZoom());
|
||||
if (currentZoom !== this.lastZoom) {
|
||||
console.log(`缩放级别变化: ${this.lastZoom} -> ${currentZoom}`);
|
||||
|
||||
// 智能计算最佳细分数量
|
||||
const performanceLevel = detectPerformanceLevel();
|
||||
const canvas = map.getCanvas();
|
||||
const viewportSize = canvas ? {
|
||||
width: canvas.width,
|
||||
height: canvas.height
|
||||
} : undefined;
|
||||
|
||||
// 获取细分建议信息
|
||||
const recommendation = getSubdivisionRecommendation(currentZoom, performanceLevel);
|
||||
console.log(`缩放级别: ${currentZoom}, 性能等级: ${performanceLevel}`);
|
||||
console.log(`细分建议: ${recommendation.subdivisions} (${recommendation.description})`);
|
||||
console.log(`三角形数量: ${recommendation.triangleCount}, 预计内存: ${recommendation.estimatedMemoryMB}MB`);
|
||||
|
||||
const meshData = RegionMeshPresets.china(currentZoom, 32);
|
||||
|
||||
if (gl instanceof WebGL2RenderingContext && this.vertexBuffer && this.indexBuffer) {
|
||||
// 更新顶点缓冲区
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, meshData.vertices, gl.STATIC_DRAW);
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
||||
|
||||
// 更新索引缓冲区
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, meshData.indices, gl.STATIC_DRAW);
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
|
||||
|
||||
this.meshData = meshData;
|
||||
this.lastZoom = currentZoom;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onAdd: function (map: maplibregl.Map, gl: WebGL2RenderingContext) {
|
||||
console.log('Custom layer added');
|
||||
},
|
||||
|
||||
onRemove: function (map: maplibregl.Map, gl: WebGL2RenderingContext) {
|
||||
// 清理WebGL资源
|
||||
if (this.program) {
|
||||
if (gl) {
|
||||
// 禁用顶点属性
|
||||
gl.disableVertexAttribArray(0); // 位置属性
|
||||
gl.disableVertexAttribArray(1); // 纹理坐标属性
|
||||
|
||||
gl.deleteProgram(this.program);
|
||||
if (this.vertexBuffer) gl.deleteBuffer(this.vertexBuffer);
|
||||
if (this.indexBuffer) gl.deleteBuffer(this.indexBuffer);
|
||||
if (this.vao) gl.deleteVertexArray(this.vao);
|
||||
if (this.tex) gl.deleteTexture(this.tex);
|
||||
if (this.lutTex) gl.deleteTexture(this.lutTex);
|
||||
}
|
||||
}
|
||||
console.log('Custom layer resources cleaned up');
|
||||
},
|
||||
|
||||
render(gl: WebGL2RenderingContext | WebGLRenderingContext, { defaultProjectionData }: CustomRenderMethodInput) {
|
||||
|
||||
if (!(gl instanceof WebGL2RenderingContext) || !this.program || !this.meshData || !this.vao) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存当前状态
|
||||
const currentProgram = gl.getParameter(gl.CURRENT_PROGRAM);
|
||||
const currentVAO = gl.getParameter(gl.VERTEX_ARRAY_BINDING);
|
||||
const blendEnabled = gl.isEnabled(gl.BLEND);
|
||||
const currentBlendSrc = gl.getParameter(gl.BLEND_SRC_ALPHA);
|
||||
const currentBlendDst = gl.getParameter(gl.BLEND_DST_ALPHA);
|
||||
|
||||
gl.useProgram(this.program);
|
||||
|
||||
// 使用缓存的uniform位置
|
||||
const locations = this.uniformLocations!;
|
||||
|
||||
if (locations['u_projection_fallback_matrix']) {
|
||||
gl.uniformMatrix4fv(
|
||||
locations['u_projection_fallback_matrix'],
|
||||
false,
|
||||
defaultProjectionData.fallbackMatrix
|
||||
);
|
||||
}
|
||||
|
||||
if (locations['u_projection_matrix']) {
|
||||
gl.uniformMatrix4fv(
|
||||
locations['u_projection_matrix'],
|
||||
false,
|
||||
defaultProjectionData.mainMatrix
|
||||
);
|
||||
}
|
||||
|
||||
if (locations['u_projection_tile_mercator_coords']) {
|
||||
gl.uniform4f(
|
||||
locations['u_projection_tile_mercator_coords'],
|
||||
...defaultProjectionData.tileMercatorCoords
|
||||
);
|
||||
}
|
||||
|
||||
if (locations['u_projection_clipping_plane']) {
|
||||
gl.uniform4f(
|
||||
locations['u_projection_clipping_plane'],
|
||||
...defaultProjectionData.clippingPlane
|
||||
);
|
||||
}
|
||||
|
||||
if (locations['u_projection_transition']) {
|
||||
gl.uniform1f(
|
||||
locations['u_projection_transition'],
|
||||
defaultProjectionData.projectionTransition
|
||||
);
|
||||
}
|
||||
|
||||
// 绑定纹理
|
||||
if (this.tex && locations['u_tex']) {
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.tex);
|
||||
gl.uniform1i(locations['u_tex'], 0);
|
||||
}
|
||||
|
||||
if (this.lutTex && locations['u_lut']) {
|
||||
gl.activeTexture(gl.TEXTURE1);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.lutTex);
|
||||
gl.uniform1i(locations['u_lut'], 1);
|
||||
}
|
||||
|
||||
gl.bindVertexArray(this.vao);
|
||||
gl.enable(gl.BLEND);
|
||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// 使用索引绘制三角形
|
||||
const indexType = this.meshData.uses32bitIndices ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT;
|
||||
const indexCount = this.meshData.indices.length;
|
||||
gl.drawElements(gl.TRIANGLES, indexCount, indexType, 0);
|
||||
|
||||
// 恢复状态
|
||||
gl.bindVertexArray(currentVAO);
|
||||
gl.useProgram(currentProgram);
|
||||
if (!blendEnabled) gl.disable(gl.BLEND);
|
||||
if (blendEnabled) gl.blendFunc(currentBlendSrc, currentBlendDst);
|
||||
}
|
||||
};
|
||||
|
||||
map.addLayer(customGlLayer);
|
||||
})
|
||||
|
||||
|
||||
@ -101,6 +419,37 @@ export function MapComponent({
|
||||
|
||||
}, [mapContainer])
|
||||
|
||||
useEffect(() => {
|
||||
if (radarTileRef.current.imgBitmap && texRef.current) {
|
||||
const gl = glRef.current
|
||||
if (!gl) return;
|
||||
|
||||
debugger
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, texRef.current)
|
||||
|
||||
// 针对灰度图优化:使用单通道RED格式,减少内存使用和提高性能
|
||||
// 虽然ImageBitmap仍是RGBA格式,但WebGL会自动将灰度值映射到RED通道
|
||||
gl.texImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
gl.RED, // 内部格式:单通道红色
|
||||
gl.RGBA, // 数据格式:ImageBitmap总是RGBA
|
||||
gl.UNSIGNED_BYTE,
|
||||
radarTileRef.current.imgBitmap
|
||||
)
|
||||
|
||||
// 设置纹理参数(如果还没有设置)
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||
}
|
||||
|
||||
}, [radarTileRef.current.imgBitmap])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={mapContainer}
|
||||
@ -109,3 +458,59 @@ export function MapComponent({
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
interface CustomGlLayer extends CustomLayerInterface {
|
||||
program?: WebGLProgram;
|
||||
aPos?: number;
|
||||
buffer?: WebGLBuffer | null;
|
||||
vao?: WebGLVertexArrayObject | null;
|
||||
meshData?: { vertices: Float32Array; indices: Uint16Array | Uint32Array; uses32bitIndices: boolean; vertexCount: number; triangleCount: number; };
|
||||
vertexBuffer?: WebGLBuffer | null;
|
||||
indexBuffer?: WebGLBuffer | null;
|
||||
lastZoom?: number; // 缓存的缩放级别
|
||||
uniformLocations?: Record<string, WebGLUniformLocation | null>; // 缓存uniform位置
|
||||
|
||||
tex?: WebGLTexture | null;
|
||||
lutTex?: WebGLTexture | null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function createLutTexture(gl: WebGL2RenderingContext) {
|
||||
const lut = new Uint8Array(256 * 4);
|
||||
for (let i = 0; i < 256; i++) {
|
||||
lut[i * 4] = i;
|
||||
lut[i * 4 + 1] = i;
|
||||
lut[i * 4 + 2] = i;
|
||||
lut[i * 4 + 3] = 255;
|
||||
}
|
||||
|
||||
const tex = gl.createTexture()
|
||||
if (!tex) {
|
||||
console.error('Failed to create texture');
|
||||
return;
|
||||
}
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex)
|
||||
gl.texImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
gl.RGBA,
|
||||
256,
|
||||
1,
|
||||
0,
|
||||
gl.RGBA,
|
||||
gl.UNSIGNED_BYTE,
|
||||
lut
|
||||
)
|
||||
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||
|
||||
return tex;
|
||||
}
|
||||
56
hooks/use-radartile.ts
Normal file
56
hooks/use-radartile.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { addDays, subDays } from 'date-fns'
|
||||
|
||||
interface UseRadarTileOptions {
|
||||
}
|
||||
|
||||
interface RadarTileStatus {
|
||||
needRefresh: boolean;
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
url: string | null;
|
||||
}
|
||||
|
||||
interface RadarTile {
|
||||
imgBitmap: ImageBitmap | null;
|
||||
needRefresh: boolean;
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
url: string | null;
|
||||
}
|
||||
|
||||
export function useRadarTile({
|
||||
}: UseRadarTileOptions = {}) {
|
||||
|
||||
const radarTileRef = useRef<RadarTile>({
|
||||
imgBitmap: null,
|
||||
needRefresh: false,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
url: null,
|
||||
})
|
||||
|
||||
|
||||
const fetchRadarTile = useCallback(async (url: string) => {
|
||||
radarTileRef.current.needRefresh = true
|
||||
radarTileRef.current.isError = false
|
||||
radarTileRef.current.url = url
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (radarTileRef.current.needRefresh) {
|
||||
if (radarTileRef.current.url) {
|
||||
fetch(radarTileRef.current.url).then(async (resp) => {
|
||||
const blob = await resp.blob()
|
||||
const imgBitmap = await createImageBitmap(blob)
|
||||
radarTileRef.current.imgBitmap = imgBitmap
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [radarTileRef.current.needRefresh, fetchRadarTile])
|
||||
|
||||
return {
|
||||
radarTileRef,
|
||||
fetchRadarTile,
|
||||
}
|
||||
}
|
||||
446
lib/tile-mesh.ts
Normal file
446
lib/tile-mesh.ts
Normal file
@ -0,0 +1,446 @@
|
||||
/**
|
||||
* 根据经纬度范围和瓦片级别生成细分网格
|
||||
* 用于globe模式下的球面渲染
|
||||
*/
|
||||
|
||||
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 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: [73, 18, 135, 54],
|
||||
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
|
||||
};
|
||||
}
|
||||
112
wind.glsl
Normal file
112
wind.glsl
Normal file
@ -0,0 +1,112 @@
|
||||
#version 300 es
|
||||
|
||||
layout(location = 0) in vec2 a_pos;
|
||||
layout(location = 1) in vec2 a_tex_coord;
|
||||
|
||||
const float PI = 3.141592653589793;
|
||||
uniform mat4 u_projection_matrix;
|
||||
#define GLOBE_RADIUS 6371008.8
|
||||
uniform highp vec4 u_projection_tile_mercator_coords;
|
||||
uniform highp vec4 u_projection_clipping_plane;
|
||||
uniform highp float u_projection_transition;
|
||||
uniform mat4 u_projection_fallback_matrix;
|
||||
|
||||
vec3 globeRotateVector(vec3 vec,vec2 angles) {
|
||||
vec3 axisRight=vec3(vec.z,0.0,-vec.x);
|
||||
vec3 axisUp=cross(axisRight,vec);
|
||||
axisRight=normalize(axisRight);
|
||||
axisUp=normalize(axisUp);
|
||||
vec2 t=tan(angles);
|
||||
return normalize(vec+axisRight*t.x+axisUp*t.y);
|
||||
}
|
||||
|
||||
mat3 globeGetRotationMatrix(vec3 spherePos) {
|
||||
vec3 axisRight=vec3(spherePos.z,0.0,-spherePos.x);
|
||||
vec3 axisDown=cross(axisRight,spherePos);
|
||||
axisRight=normalize(axisRight);
|
||||
axisDown=normalize(axisDown);
|
||||
return mat3(axisRight,axisDown,spherePos);
|
||||
}
|
||||
|
||||
float circumferenceRatioAtTileY(float tileY) {
|
||||
float mercator_pos_y = u_projection_tile_mercator_coords.y + u_projection_tile_mercator_coords.w*tileY;
|
||||
float spherical_y = 2.0*atan(exp(PI-(mercator_pos_y*PI*2.0))) - PI*0.5;return cos(spherical_y);
|
||||
}
|
||||
|
||||
float projectLineThickness(float tileY) {
|
||||
float thickness=1.0/circumferenceRatioAtTileY(tileY);
|
||||
if (u_projection_transition < 0.999) {
|
||||
return mix(1.0,thickness,u_projection_transition);
|
||||
} else {
|
||||
return thickness;
|
||||
}
|
||||
}
|
||||
|
||||
vec3 projectToSphere(vec2 translatedPos,vec2 rawPos) {
|
||||
vec2 mercator_pos = u_projection_tile_mercator_coords.xy+u_projection_tile_mercator_coords.zw*translatedPos;
|
||||
vec2 spherical;
|
||||
spherical.x=mercator_pos.x*PI*2.0+PI;
|
||||
spherical.y=2.0*atan(exp(PI-(mercator_pos.y*PI*2.0)))-PI*0.5;
|
||||
float len=cos(spherical.y);
|
||||
vec3 pos=vec3(sin(spherical.x)*len,sin(spherical.y),cos(spherical.x)*len);
|
||||
if (rawPos.y <-32767.5) {
|
||||
pos = vec3(0.0,1.0,0.0);
|
||||
}
|
||||
if (rawPos.y > 32766.5) {
|
||||
pos=vec3(0.0,-1.0,0.0);
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
vec3 projectToSphere(vec2 posInTile) {
|
||||
return projectToSphere(posInTile,vec2(0.0,0.0));
|
||||
}
|
||||
|
||||
float globeComputeClippingZ(vec3 spherePos) {
|
||||
return (1.0-(dot(spherePos,u_projection_clipping_plane.xyz)+u_projection_clipping_plane.w));
|
||||
}
|
||||
|
||||
vec4 interpolateProjection(vec2 posInTile,vec3 spherePos,float elevation) {
|
||||
vec3 elevatedPos=spherePos*(1.0+elevation/GLOBE_RADIUS);
|
||||
vec4 globePosition=u_projection_matrix*vec4(elevatedPos,1.0);
|
||||
globePosition.z=globeComputeClippingZ(elevatedPos)*globePosition.w;
|
||||
|
||||
if (u_projection_transition > 0.999) {return globePosition;}
|
||||
|
||||
vec4 flatPosition=u_projection_fallback_matrix*vec4(posInTile,elevation,1.0);
|
||||
const float z_globeness_threshold=0.2;
|
||||
|
||||
vec4 result=globePosition;result.z=mix(0.0,globePosition.z,clamp((u_projection_transition-z_globeness_threshold)/(1.0-z_globeness_threshold),0.0,1.0));result.xyw=mix(flatPosition.xyw,globePosition.xyw,u_projection_transition);if ((posInTile.y <-32767.5) || (posInTile.y > 32766.5)) {result=globePosition;const float poles_hidden_anim_percentage=0.02;result.z=mix(globePosition.z,100.0,pow(max((1.0-u_projection_transition)/poles_hidden_anim_percentage,0.0),8.0));}return result;}vec4 interpolateProjectionFor3D(vec2 posInTile,vec3 spherePos,float elevation) {vec3 elevatedPos=spherePos*(1.0+elevation/GLOBE_RADIUS);vec4 globePosition=u_projection_matrix*vec4(elevatedPos,1.0);
|
||||
|
||||
if (u_projection_transition > 0.999) {return globePosition;}
|
||||
|
||||
vec4 fallbackPosition=u_projection_fallback_matrix*vec4(posInTile,elevation,1.0);
|
||||
return mix(fallbackPosition,globePosition,u_projection_transition);
|
||||
}
|
||||
|
||||
vec4 projectTile(vec2 posInTile) {
|
||||
return interpolateProjection(posInTile,projectToSphere(posInTile),0.0);
|
||||
}
|
||||
|
||||
vec4 projectTile(vec2 posInTile,vec2 rawPos) {
|
||||
return interpolateProjection(posInTile,projectToSphere(posInTile,rawPos),0.0);
|
||||
}
|
||||
|
||||
vec4 projectTileWithElevation(vec2 posInTile,float elevation) {
|
||||
return interpolateProjection(posInTile,projectToSphere(posInTile),elevation);
|
||||
}
|
||||
|
||||
vec4 projectTileFor3D(vec2 posInTile,float elevation) {
|
||||
vec3 spherePos=projectToSphere(posInTile,posInTile);
|
||||
return interpolateProjectionFor3D(posInTile,spherePos,elevation);
|
||||
}
|
||||
|
||||
#define GLOBE
|
||||
|
||||
out vec2 v_tex_coord;
|
||||
|
||||
void main() {
|
||||
gl_Position = projectTile(a_pos);
|
||||
v_tex_coord = a_tex_coord;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user