feat: renderiza Via Láctea como textura procedural contínua
This commit is contained in:
parent
96486b447c
commit
d8d99903d6
1 changed files with 148 additions and 126 deletions
|
|
@ -1264,17 +1264,6 @@ function glareFactor(x, y, W, H, on) {
|
|||
return Math.max(0, Math.min(1, (d - 150) / 240));
|
||||
}
|
||||
|
||||
/* ── Via Láctea: partículas geradas em coords galácticas (RNG com semente,
|
||||
estável entre reloads) e convertidas p/ equatorial J2000 uma única vez ── */
|
||||
function mulberry32(seed) {
|
||||
return function () {
|
||||
seed |= 0; seed = seed + 0x6D2B79F5 | 0;
|
||||
let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
|
||||
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
||||
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
/* Transposta da matriz equatorial->galáctica (Hipparcos, J2000) */
|
||||
const G2E = [
|
||||
[-0.0548755604, 0.4941094279, -0.8676661490],
|
||||
|
|
@ -1282,94 +1271,151 @@ const G2E = [
|
|||
[-0.4838350155, 0.7469822445, 0.4559837762],
|
||||
];
|
||||
|
||||
/* ── Via Láctea estilizada: fita suave + poucas nuvens grandes ──
|
||||
Âncora astronômica real (equador galáctico -> gira com o céu de verdade),
|
||||
mas pintura surrealista: sprites grandes e macios, sem ruído de pontos */
|
||||
function galToEq(l, b) {
|
||||
const cb = Math.cos(b * DEG);
|
||||
const gx = cb * Math.cos(l * DEG), gy = cb * Math.sin(l * DEG), gz = Math.sin(b * DEG);
|
||||
return {
|
||||
ra: (Math.atan2(
|
||||
G2E[1][0] * gx + G2E[1][1] * gy + G2E[1][2] * gz,
|
||||
G2E[0][0] * gx + G2E[0][1] * gy + G2E[0][2] * gz) / DEG + 360) % 360,
|
||||
dec: Math.asin(G2E[2][0] * gx + G2E[2][1] * gy + G2E[2][2] * gz) / DEG,
|
||||
};
|
||||
}
|
||||
|
||||
/* Cor por distância angular ao centro galáctico: creme-âmbar -> neutro -> azul */
|
||||
function mwColor(dl) {
|
||||
const t = Math.min(1, dl / 140);
|
||||
const mix = (a, b2, c) => Math.round(t < 0.5 ? a + (b2 - a) * t * 2 : b2 + (c - b2) * (t - 0.5) * 2);
|
||||
return [mix(255, 236, 186), mix(226, 224, 205), mix(188, 214, 242)];
|
||||
}
|
||||
|
||||
const mwBand = []; // fita + nuvens (aditivo) {ra, dec, size, alpha, ci}
|
||||
const mwShadow = []; // Grande Fenda (recorte) {ra, dec, size, alpha}
|
||||
(function buildMilkyWay() {
|
||||
/* fita contínua: sprites grandes bem sobrepostos ao longo de b~0 */
|
||||
for (let l = 0; l < 360; l += 2.5) {
|
||||
const dl = Math.min(l, 360 - l);
|
||||
const central = Math.exp(-((dl / 80) ** 2));
|
||||
const wobble = 1.2 * Math.sin(l * DEG * 3.1) + 0.8 * Math.sin(l * DEG * 1.3 + 2);
|
||||
const eq = galToEq(l, wobble * 0.6);
|
||||
mwBand.push({
|
||||
ra: eq.ra, dec: eq.dec,
|
||||
size: 170 + 170 * central,
|
||||
alpha: 0.05 + 0.13 * central,
|
||||
ci: mwColor(dl),
|
||||
});
|
||||
}
|
||||
/* bojo + poucas nuvens grandes em regiões reais (Carina, Cygnus...) */
|
||||
const clouds = [
|
||||
{ l: 0, b: 0, s: 420, a: 0.20 }, // bojo central (Sagitário)
|
||||
{ l: 8, b: -1.5, s: 300, a: 0.15 },
|
||||
{ l: 352, b: 1.5, s: 300, a: 0.15 },
|
||||
{ l: 25, b: 1, s: 240, a: 0.11 }, // Scutum
|
||||
{ l: 330, b: -1, s: 260, a: 0.12 },
|
||||
{ l: 55, b: 0.5, s: 210, a: 0.09 },
|
||||
{ l: 285, b: -0.5, s: 230, a: 0.10 }, // Carina
|
||||
{ l: 80, b: 1, s: 190, a: 0.08 }, // Cygnus
|
||||
];
|
||||
for (const c of clouds) {
|
||||
const eq = galToEq(c.l, c.b);
|
||||
mwBand.push({ ra: eq.ra, dec: eq.dec, size: c.s, alpha: c.a,
|
||||
ci: mwColor(Math.min(c.l, 360 - c.l)) });
|
||||
}
|
||||
/* Grande Fenda: sombras grandes e macias serpenteando sobre a fita */
|
||||
for (let l = -58; l <= 42; l += 6) {
|
||||
const eq = galToEq((l + 360) % 360, -0.6 + 1.6 * Math.sin(l * 0.05 + 0.8));
|
||||
mwShadow.push({
|
||||
ra: eq.ra, dec: eq.dec,
|
||||
size: 150 + 90 * Math.abs(Math.sin(l * 0.11 + 2)),
|
||||
alpha: 0.45 + 0.30 * Math.abs(Math.sin(l * 0.07)),
|
||||
});
|
||||
}
|
||||
/* Saco de Carvão — mancha escura junto ao Cruzeiro do Sul */
|
||||
const cs = galToEq(303, -1.5);
|
||||
mwShadow.push({ ra: cs.ra, dec: cs.dec, size: 170, alpha: 0.8 });
|
||||
})();
|
||||
|
||||
/* Sprites macios (quente / neutro / frio) + sprite de sombra p/ recorte */
|
||||
function makeMwSprite(r, g, b) {
|
||||
const c = document.createElement('canvas');
|
||||
c.width = c.height = 64;
|
||||
const s = c.getContext('2d');
|
||||
const grad = s.createRadialGradient(32, 32, 0, 32, 32, 32);
|
||||
grad.addColorStop(0, `rgba(${r},${g},${b},1)`);
|
||||
grad.addColorStop(0.5, `rgba(${r},${g},${b},0.4)`);
|
||||
grad.addColorStop(1, `rgba(${r},${g},${b},0)`);
|
||||
s.fillStyle = grad;
|
||||
s.fillRect(0, 0, 64, 64);
|
||||
return c;
|
||||
}
|
||||
const mwSprites = [makeMwSprite(255, 226, 188), // quente (núcleo)
|
||||
makeMwSprite(236, 224, 214), // neutro
|
||||
makeMwSprite(188, 214, 242)]; // frio (anticentro)
|
||||
const mwShadowSprite = makeMwSprite(255, 255, 255); // recorte via destination-out
|
||||
|
||||
/* camada própria da fita — a fenda é recortada antes de compor no céu */
|
||||
/* ── Via Láctea procedural contínua ──
|
||||
A textura é calculada no espaço galáctico em uma camada de baixa resolução.
|
||||
Assim a faixa, o bojo e a poeira acompanham a projeção real sem denunciar
|
||||
sprites circulares ou custar um passe em resolução cheia. */
|
||||
const MW_RENDER_SCALE = 0.25;
|
||||
const mwLayer = document.createElement('canvas');
|
||||
function spriteFor(ci) { return mwSprites[ci[2] > ci[0] ? 2 : (ci[0] > 240 ? 0 : 1)]; }
|
||||
|
||||
function mwHash(ix, iy) {
|
||||
let h = Math.imul(ix, 374761393) + Math.imul(iy, 668265263);
|
||||
h = Math.imul(h ^ (h >>> 13), 1274126177);
|
||||
return ((h ^ (h >>> 16)) >>> 0) / 4294967295;
|
||||
}
|
||||
|
||||
function mwValueNoise(x, y, periodX) {
|
||||
const fx = Math.floor(x), fy = Math.floor(y);
|
||||
const tx0 = x - fx, ty0 = y - fy;
|
||||
const tx = tx0 * tx0 * (3 - 2 * tx0);
|
||||
const ty = ty0 * ty0 * (3 - 2 * ty0);
|
||||
const wrap = n => ((n % periodX) + periodX) % periodX;
|
||||
const a = mwHash(wrap(fx), fy);
|
||||
const b = mwHash(wrap(fx + 1), fy);
|
||||
const c = mwHash(wrap(fx), fy + 1);
|
||||
const d = mwHash(wrap(fx + 1), fy + 1);
|
||||
const top = a + (b - a) * tx;
|
||||
const bot = c + (d - c) * tx;
|
||||
return top + (bot - top) * ty;
|
||||
}
|
||||
|
||||
/* Ruído periódico em longitude: não deixa emenda em l=0°/360°. */
|
||||
function mwFbm(l, b) {
|
||||
let value = 0, norm = 0, amp = 1;
|
||||
for (const period of [4, 8, 16, 32]) {
|
||||
value += amp * mwValueNoise(l / 360 * period, (b + 90) / 360 * period, period);
|
||||
norm += amp;
|
||||
amp *= 0.52;
|
||||
}
|
||||
return value / norm;
|
||||
}
|
||||
|
||||
function mwAngularDistance(l, target) {
|
||||
const d = Math.abs(l - target);
|
||||
return Math.min(d, 360 - d);
|
||||
}
|
||||
|
||||
/* Densidade luminosa em coordenadas galácticas. O detalhe vem do ruído,
|
||||
enquanto as formas grandes continuam astronomicamente reconhecíveis. */
|
||||
function mwDensity(l, b) {
|
||||
const dl = Math.min(l, 360 - l);
|
||||
const core = Math.exp(-((dl / 72) ** 2));
|
||||
const width = 4.2 + 5.2 * core;
|
||||
const cloud = mwFbm(l, b);
|
||||
const fine = mwFbm((l * 2) % 360, b * 2.2);
|
||||
|
||||
let density = Math.exp(-0.5 * (b / width) ** 2) *
|
||||
(0.34 + 1.05 * cloud ** 1.7);
|
||||
density += 0.16 * Math.exp(-0.5 * (b / (width * 2.6)) ** 2) *
|
||||
(0.45 + 0.55 * cloud);
|
||||
density += 1.05 * Math.exp(-0.5 * ((dl / 19) ** 2 + (b / 11) ** 2)) *
|
||||
(0.65 + 0.45 * cloud);
|
||||
|
||||
/* Grande Fenda: duas trilhas estreitas, deformadas e com força irregular. */
|
||||
const riftReach = 1 - Math.max(0, Math.min(1, (dl - 70) / 55));
|
||||
const lr = l * DEG;
|
||||
const riftA = 0.7 + 1.45 * Math.sin(lr * 2.1 + 0.5) + 0.45 * Math.sin(lr * 7.0);
|
||||
const riftB = -2.3 + 0.75 * Math.sin(lr * 1.4 + 2.2);
|
||||
const darkA = Math.exp(-0.5 * ((b - riftA) / (1.0 + 0.7 * fine)) ** 2) *
|
||||
(0.58 + 0.28 * fine);
|
||||
const darkB = Math.exp(-0.5 * ((b - riftB) / 1.15) ** 2) * 0.48;
|
||||
density *= 1 - Math.min(0.90, (darkA + darkB) * riftReach);
|
||||
|
||||
/* Saco de Carvão junto ao Cruzeiro do Sul. */
|
||||
const coal = Math.exp(-0.5 * ((mwAngularDistance(l, 303) / 3.8) ** 2 +
|
||||
((b + 1.5) / 2.8) ** 2));
|
||||
density *= 1 - 0.84 * coal;
|
||||
|
||||
return Math.max(0, density * (0.24 + 0.76 * Math.exp(-((dl / 105) ** 2))));
|
||||
}
|
||||
|
||||
function mwRgb(dl) {
|
||||
const t = Math.min(1, dl / 145);
|
||||
const warm = [255, 220, 181], neutral = [222, 226, 235], cool = [177, 207, 244];
|
||||
const a = t < 0.5 ? warm : neutral;
|
||||
const z = t < 0.5 ? neutral : cool;
|
||||
const u = t < 0.5 ? t * 2 : (t - 0.5) * 2;
|
||||
return [a[0] + (z[0] - a[0]) * u,
|
||||
a[1] + (z[1] - a[1]) * u,
|
||||
a[2] + (z[2] - a[2]) * u];
|
||||
}
|
||||
|
||||
function renderMilkyWay(W, H, horizonPx, lst, glareOn) {
|
||||
const rw = Math.max(1, Math.ceil(W * MW_RENDER_SCALE));
|
||||
const rh = Math.max(1, Math.ceil(horizonPx * MW_RENDER_SCALE));
|
||||
if (mwLayer.width !== rw || mwLayer.height !== rh) {
|
||||
mwLayer.width = rw;
|
||||
mwLayer.height = rh;
|
||||
}
|
||||
const mctx = mwLayer.getContext('2d');
|
||||
const image = mctx.createImageData(rw, rh);
|
||||
const pixels = image.data;
|
||||
const sl = Math.sin(obsLat * DEG), cl = Math.cos(obsLat * DEG);
|
||||
const lstRad = lst * DEG, cosLst = Math.cos(lstRad), sinLst = Math.sin(lstRad);
|
||||
|
||||
const azSin = new Float32Array(rw), azCos = new Float32Array(rw);
|
||||
for (let x = 0; x < rw; x++) {
|
||||
const az = (VIEW_AZ + ((x + 0.5) / rw - 0.5) * FOV_AZ) * DEG;
|
||||
azSin[x] = Math.sin(az);
|
||||
azCos[x] = Math.cos(az);
|
||||
}
|
||||
|
||||
let offset = 0;
|
||||
for (let y = 0; y < rh; y++) {
|
||||
const screenY = (y + 0.5) / rh * horizonPx;
|
||||
const alt = ALT_TOP * (1 - screenY / horizonPx);
|
||||
const sa = Math.sin(alt * DEG), ca = Math.cos(alt * DEG);
|
||||
const ext = extinction(alt);
|
||||
for (let x = 0; x < rw; x++, offset += 4) {
|
||||
const north = ca * azCos[x], east = ca * azSin[x];
|
||||
const hourX = -north * sl + sa * cl;
|
||||
const ez = north * cl + sa * sl;
|
||||
const ex = cosLst * hourX - sinLst * east;
|
||||
const ey = sinLst * hourX + cosLst * east;
|
||||
|
||||
/* E2G = transposta de G2E. */
|
||||
const gx = G2E[0][0] * ex + G2E[1][0] * ey + G2E[2][0] * ez;
|
||||
const gy = G2E[0][1] * ex + G2E[1][1] * ey + G2E[2][1] * ez;
|
||||
const gz = G2E[0][2] * ex + G2E[1][2] * ey + G2E[2][2] * ez;
|
||||
const l = (Math.atan2(gy, gx) / DEG + 360) % 360;
|
||||
const b = Math.asin(Math.max(-1, Math.min(1, gz))) / DEG;
|
||||
if (Math.abs(b) > 35) continue;
|
||||
|
||||
const density = mwDensity(l, b);
|
||||
if (density < 0.012) continue;
|
||||
const screenX = (x + 0.5) / rw * W;
|
||||
const glare = glareFactor(screenX, screenY, W, H, glareOn);
|
||||
const alpha = Math.min(0.26, density * 0.145) * ext * glare;
|
||||
if (alpha < 0.002) continue;
|
||||
const rgb = mwRgb(Math.min(l, 360 - l));
|
||||
pixels[offset] = rgb[0];
|
||||
pixels[offset + 1] = rgb[1];
|
||||
pixels[offset + 2] = rgb[2];
|
||||
pixels[offset + 3] = alpha * 255;
|
||||
}
|
||||
}
|
||||
mctx.putImageData(image, 0, 0);
|
||||
}
|
||||
|
||||
/* ── Passo de projeção: recalcula posições de tela (a cada 30s o céu gira
|
||||
~0.125°, imperceptível). Camada estática (Via Láctea + estrelas fracas)
|
||||
|
|
@ -1390,34 +1436,10 @@ function projectSky() {
|
|||
!document.body.classList.contains('chuva');
|
||||
twinkList = [];
|
||||
|
||||
/* fita da Via Láctea na camada própria, fenda recortada, depois composta */
|
||||
const sScale = H / 1080;
|
||||
mwLayer.width = W;
|
||||
mwLayer.height = H;
|
||||
const mctx = mwLayer.getContext('2d');
|
||||
mctx.globalCompositeOperation = 'lighter';
|
||||
for (const p of mwBand) {
|
||||
const aa = altAz(p.ra, p.dec, lst);
|
||||
const sc = skyToScreen(aa.alt, aa.az, W, horizonPx);
|
||||
if (!sc) continue;
|
||||
const a = p.alpha * extinction(aa.alt) * glareFactor(sc.x, sc.y, W, H, glareOn);
|
||||
if (a < 0.004) continue;
|
||||
const s = p.size * sScale;
|
||||
mctx.globalAlpha = a;
|
||||
mctx.drawImage(spriteFor(p.ci), sc.x - s / 2, sc.y - s / 2, s, s);
|
||||
}
|
||||
mctx.globalCompositeOperation = 'destination-out';
|
||||
for (const p of mwShadow) {
|
||||
const aa = altAz(p.ra, p.dec, lst);
|
||||
const sc = skyToScreen(aa.alt, aa.az, W, horizonPx);
|
||||
if (!sc) continue;
|
||||
const s = p.size * sScale;
|
||||
mctx.globalAlpha = p.alpha;
|
||||
mctx.drawImage(mwShadowSprite, sc.x - s / 2, sc.y - s / 2, s, s);
|
||||
}
|
||||
mctx.globalAlpha = 1;
|
||||
mctx.globalCompositeOperation = 'source-over';
|
||||
bctx.drawImage(mwLayer, 0, 0);
|
||||
renderMilkyWay(W, H, horizonPx, lst, glareOn);
|
||||
bctx.imageSmoothingEnabled = true;
|
||||
bctx.imageSmoothingQuality = 'high';
|
||||
bctx.drawImage(mwLayer, 0, 0, W, horizonPx);
|
||||
|
||||
for (let i = 0; i < STAR_DATA.length; i++) {
|
||||
const [ra, dec, mag, ci] = STAR_DATA[i];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue