radar layer
This commit is contained in:
parent
33af6ca468
commit
8e847d44a8
@ -25,6 +25,8 @@ import {
|
|||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useTimeline } from '@/hooks/use-timeline';
|
import { useTimeline } from '@/hooks/use-timeline';
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useRadarTile } from '@/hooks/use-radartile'
|
||||||
|
|
||||||
|
|
||||||
export default function Page() {
|
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 startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // 7天前
|
||||||
const endDate = new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000); // 3天后
|
const endDate = new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000); // 3天后
|
||||||
const { setTime } = useTimeline()
|
const { setTime } = useTimeline()
|
||||||
|
const { fetchRadarTile } = useRadarTile({})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchRadarTile("http://127.0.0.1:3050/test")
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<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) => {
|
const handleWheel = (e: WheelEvent) => {
|
||||||
@ -899,56 +811,7 @@ export const Timeline: React.FC<Props> = ({
|
|||||||
// 添加全局鼠标抬起事件,防止鼠标移出canvas后拖拽卡住
|
// 添加全局鼠标抬起事件,防止鼠标移出canvas后拖拽卡住
|
||||||
document.addEventListener('mouseup', handleMouseUp);
|
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 () => {
|
return () => {
|
||||||
// 移除事件监听器
|
// 移除事件监听器
|
||||||
@ -980,14 +843,7 @@ export const Timeline: React.FC<Props> = ({
|
|||||||
// 重置临时状态(不需要调用setState,因为组件即将卸载或重新初始化)
|
// 重置临时状态(不需要调用setState,因为组件即将卸载或重新初始化)
|
||||||
panTempOffsetRef.current = 0;
|
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]);
|
}, [boxSize, actualVesicaData]);
|
||||||
|
|
||||||
@ -1043,8 +899,115 @@ export const Timeline: React.FC<Props> = ({
|
|||||||
gl.bindVertexArray(null);
|
gl.bindVertexArray(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}, [state.zoomLevel, state.panOffset, startDate, endDate, currentDate]);
|
}, [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 (
|
return (
|
||||||
<div className={cn(props.className, "w-full h-12 flex flex-row")}>
|
<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)' }}>
|
<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'
|
'use client'
|
||||||
|
|
||||||
import React, { useEffect, useRef } from 'react'
|
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 'maplibre-gl/dist/maplibre-gl.css'
|
||||||
import { useMap } from '@/app/map-context'
|
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 { useMapLocation } from '@/hooks/use-map-location'
|
||||||
|
import { createOptimalWorldMesh, getSubdivisionRecommendation, detectPerformanceLevel, createOptimalRegionMesh, RegionMeshPresets } from '@/lib/tile-mesh'
|
||||||
|
import { useRadarTile } from '@/hooks/use-radartile'
|
||||||
|
|
||||||
interface MapComponentProps {
|
interface MapComponentProps {
|
||||||
style?: string
|
style?: string
|
||||||
@ -32,6 +24,10 @@ export function MapComponent({
|
|||||||
const mapContainer = useRef<HTMLDivElement>(null)
|
const mapContainer = useRef<HTMLDivElement>(null)
|
||||||
const { setMap } = useMap()
|
const { setMap } = useMap()
|
||||||
const { location } = useMapLocation()
|
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(() => {
|
useEffect(() => {
|
||||||
if (!mapContainer.current) return
|
if (!mapContainer.current) return
|
||||||
@ -79,21 +75,343 @@ export function MapComponent({
|
|||||||
type: 'globe'
|
type: 'globe'
|
||||||
})
|
})
|
||||||
|
|
||||||
map.addSource('nexrad', {
|
// map.addSource('nexrad', {
|
||||||
type: 'raster',
|
// type: 'raster',
|
||||||
tiles: [
|
// 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'
|
// // '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'
|
// 'http://127.0.0.1:3050/tiles/{z}/{x}/{y}?time=202507220012'
|
||||||
],
|
// ],
|
||||||
tileSize: 256
|
// tileSize: 256
|
||||||
});
|
// });
|
||||||
|
|
||||||
map.addLayer({
|
const customGlLayer: CustomGlLayer = {
|
||||||
id: 'nexrad-layer',
|
id: 'player',
|
||||||
type: 'raster',
|
type: 'custom',
|
||||||
source: 'nexrad',
|
lastZoom: -1, // 添加缓存的缩放级别
|
||||||
paint: { 'raster-opacity': 0.8 }
|
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])
|
}, [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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={mapContainer}
|
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