From 8e847d44a8bde0790a962e36bd83ea4062177e97 Mon Sep 17 00:00:00 2001 From: tsuki Date: Fri, 25 Jul 2025 22:39:08 +0800 Subject: [PATCH] radar layer --- app/page.tsx | 7 + app/timeline.tsx | 251 ++++++++----------- components/map-component.tsx | 457 +++++++++++++++++++++++++++++++++-- hooks/use-radartile.ts | 56 +++++ lib/tile-mesh.ts | 446 ++++++++++++++++++++++++++++++++++ wind.glsl | 112 +++++++++ wind.js | 0 7 files changed, 1159 insertions(+), 170 deletions(-) create mode 100644 hooks/use-radartile.ts create mode 100644 lib/tile-mesh.ts create mode 100644 wind.glsl delete mode 100644 wind.js diff --git a/app/page.tsx b/app/page.tsx index a2b0889..e835dea 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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 ( diff --git a/app/timeline.tsx b/app/timeline.tsx index 1ff5e9e..7db1dac 100644 --- a/app/timeline.tsx +++ b/app/timeline.tsx @@ -707,95 +707,7 @@ export const Timeline: React.FC = ({ } - 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 = ({ // 添加全局鼠标抬起事件,防止鼠标移出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 = ({ // 重置临时状态(不需要调用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 = ({ 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 (
diff --git a/components/map-component.tsx b/components/map-component.tsx index 15635e6..53f5cf5 100644 --- a/components/map-component.tsx +++ b/components/map-component.tsx @@ -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(null) const { setMap } = useMap() const { location } = useMapLocation() + const { radarTileRef } = useRadarTile() + const texRef = useRef(null) + const lutTexRef = useRef(null) + const glRef = useRef(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, // 缓存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 (
) -} \ No newline at end of file +} + + +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; // 缓存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; +} \ No newline at end of file diff --git a/hooks/use-radartile.ts b/hooks/use-radartile.ts new file mode 100644 index 0000000..bc2f75b --- /dev/null +++ b/hooks/use-radartile.ts @@ -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({ + 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, + } +} \ No newline at end of file diff --git a/lib/tile-mesh.ts b/lib/tile-mesh.ts new file mode 100644 index 0000000..d1434a8 --- /dev/null +++ b/lib/tile-mesh.ts @@ -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 + }; +} \ No newline at end of file diff --git a/wind.glsl b/wind.glsl new file mode 100644 index 0000000..3eb3776 --- /dev/null +++ b/wind.glsl @@ -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; +} \ No newline at end of file diff --git a/wind.js b/wind.js deleted file mode 100644 index e69de29..0000000