mirror of
git://f0xx.org/ac/ac-be-hub
synced 2026-08-09 20:59:23 +03:00
Hub globe v2: mercator wrap, GPU path, landing layout updates.
Refine 3D globe rendering, layer CSS, landing page wiring and TOC UX. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
225
assets/hub-globe-gpu.js
Normal file
225
assets/hub-globe-gpu.js
Normal file
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* 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);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user