/** * WebGL land disk (globe3d=11) — GPU orthographic mercator sample; ocean/sun stay Canvas2D. */ const VS = ` attribute vec2 aPos; void main() { gl_Position = vec4(aPos, 0.0, 1.0); } `; const FS = ` precision highp float; uniform vec3 uDisk; uniform float uRotY; uniform sampler2D uMerc; uniform float uLatf; uniform float uLngf; uniform float uZy; uniform float uZx; uniform float uPp; uniform float uPe; uniform float uLonCenter; uniform float uLimbZ; const float PI = 3.141592653589793; const float TAU = 6.283185307179586; float wrapLon(float lon) { lon = mod(lon + PI, TAU); if (lon < 0.0) lon += TAU; return lon - PI; } float latToMercatorV(float lat) { float clamped = clamp(lat, -1.52, 1.52); float t = 0.5 + asinh(tan(clamped)) / TAU; return clamp(1.0 - t, 0.0, 1.0); } vec2 landZoomLonLat(float lon, float lat, vec3 tex) { float latT = min(1.0, abs(lat) / (PI * 0.5)); float lonT = min(1.0, abs(lon) / PI); float rimT = min(1.0, length(tex.xz)); float latMul = uLatf * (uZy * (1.0 - latT * latT) + latT * latT); float lonMul = uLngf * (uZx * (1.0 - lonT * lonT) + lonT * lonT); float poleLens = 1.0 - uPp * latT * latT; float edgeLens = 1.0 - uPe * rimT * rimT; return vec2( wrapLon(lon * lonMul * edgeLens + uLonCenter), clamp(lat * latMul * poleLens, -1.52, 1.52) ); } void main() { vec2 p = (gl_FragCoord.xy - uDisk.xy) / uDisk.z; float rr = dot(p, p); if (rr > 1.0) { discard; } float nz = sqrt(max(0.0, 1.0 - rr)); if (nz <= uLimbZ) { discard; } float cosR = cos(uRotY); float sinR = sin(uRotY); float bx = p.x * cosR - nz * sinR; float by = p.y; float bz = p.x * sinR + nz * cosR; vec3 tex = vec3(bx, by, bz); float lon = atan(bx, bz); float lat = asin(clamp(by, -1.0, 1.0)); vec2 mapped = landZoomLonLat(lon, lat, tex); float u = fract((mapped.x + PI) / TAU); float v = latToMercatorV(mapped.y); vec4 merc = texture2D(uMerc, vec2(u, v)); float alpha = merc.a / 255.0; if (alpha <= 0.004) { discard; } gl_FragColor = vec4(merc.rgb / 255.0, alpha); } `; function compileShader(gl, type, src) { const sh = gl.createShader(type); gl.shaderSource(sh, src); gl.compileShader(sh); if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) { console.warn('Globe GPU shader:', gl.getShaderInfoLog(sh)); gl.deleteShader(sh); return null; } return sh; } function buildMercTexture(gl, cache, cs) { const w = cache.w; const h = cache.h; const pixels = new Uint8Array(w * h * 4); for (let i = 0; i < w * h; i++) { const k = cache.kind[i]; if (!k) { continue; } let r = cache.rgb[i * 3]; let g = cache.rgb[i * 3 + 1]; let b = cache.rgb[i * 3 + 2]; let a = cs.fillAlpha; if (k === 2 || k === 3) { r = Math.min(255, r + cs.borderBoostR); g = Math.min(255, g + cs.borderBoostG); b = Math.min(255, b + cs.borderBoostB); a = cs.borderAlpha; } const o = i * 4; pixels[o] = r; pixels[o + 1] = g; pixels[o + 2] = b; pixels[o + 3] = a; } const tex = gl.createTexture(); 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.REPEAT); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, pixels); gl.bindTexture(gl.TEXTURE_2D, null); return tex; } /** * @param {HTMLCanvasElement} canvas * @param {{ cache: object, continentStyle: object, landZoom: object, lonCenter: number }} opts */ export function createGpuGlobeRenderer(canvas, opts) { const gl = canvas.getContext('webgl', { alpha: true, antialias: false, depth: false, stencil: false, preserveDrawingBuffer: false, powerPreference: 'high-performance', }); if (!gl) { return null; } const vs = compileShader(gl, gl.VERTEX_SHADER, VS); const fs = compileShader(gl, gl.FRAGMENT_SHADER, FS); if (!vs || !fs) { return null; } const prog = gl.createProgram(); gl.attachShader(prog, vs); gl.attachShader(prog, fs); gl.linkProgram(prog); if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { console.warn('Globe GPU link:', gl.getProgramInfoLog(prog)); return null; } gl.deleteShader(vs); gl.deleteShader(fs); const buf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW); const cs = opts.continentStyle; const mercTex = buildMercTexture(gl, opts.cache, cs); const lz = opts.landZoom; const loc = { aPos: gl.getAttribLocation(prog, 'aPos'), uDisk: gl.getUniformLocation(prog, 'uDisk'), uRotY: gl.getUniformLocation(prog, 'uRotY'), uMerc: gl.getUniformLocation(prog, 'uMerc'), uLatf: gl.getUniformLocation(prog, 'uLatf'), uLngf: gl.getUniformLocation(prog, 'uLngf'), uZy: gl.getUniformLocation(prog, 'uZy'), uZx: gl.getUniformLocation(prog, 'uZx'), uPp: gl.getUniformLocation(prog, 'uPp'), uPe: gl.getUniformLocation(prog, 'uPe'), uLonCenter: gl.getUniformLocation(prog, 'uLonCenter'), uLimbZ: gl.getUniformLocation(prog, 'uLimbZ'), }; gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); return { draw(cx, cy, r, rotY, width, height) { gl.viewport(0, 0, width, height); gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); gl.useProgram(prog); gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.enableVertexAttribArray(loc.aPos); gl.vertexAttribPointer(loc.aPos, 2, gl.FLOAT, false, 0, 0); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, mercTex); gl.uniform1i(loc.uMerc, 0); gl.uniform3f(loc.uDisk, cx, cy, r); gl.uniform1f(loc.uRotY, rotY); gl.uniform1f(loc.uLatf, lz.latf); gl.uniform1f(loc.uLngf, lz.lngf); gl.uniform1f(loc.uZy, lz.zy); gl.uniform1f(loc.uZx, lz.zx); gl.uniform1f(loc.uPp, lz.pp); gl.uniform1f(loc.uPe, lz.pe); gl.uniform1f(loc.uLonCenter, opts.lonCenter); gl.uniform1f(loc.uLimbZ, 0.002); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); }, destroy() { gl.deleteTexture(mercTex); gl.deleteBuffer(buf); gl.deleteProgram(prog); }, }; }