按用户参考图(破碎月亮)对齐的整套真实感升级: - 月球: SSS/NASA 2K真实纹理+运行时亮度转凹凸, 顶点撕裂伤口(不规则锯齿轮廓)+发光裂缝(emissive放射纹+断面代码)+伤口辉光脉动 - 破碎: 2块大碎片(顶点扰动)+220块Instanced碎石量级谱(伤口切线拖带,各自漂移翻滚)+5团青灰尘埃云(烟尘流动) - 太空: 真实星野天球(SSS银河2K)替换程序点星, 保留近景尘埃视差层 - 太阳: 亮度>1的小烈核心(推过泛光阈值)+克制光晕+变形宽银幕横向光纹 - 渲染: ACES电影色调映射+UnrealBloomPass泛光(three.module+8个后处理文件本地化,importmap加载) - 移动端降级: 关泛光+减粒子 - 页脚: NASA/SSS纹理CC-BY署名
643 lines
26 KiB
JavaScript
643 lines
26 KiB
JavaScript
/* ============================================================
|
|
晟算科技 · 太空场景引擎 v3 —— 电影级真实感
|
|
真实月面(NASA/SSS) + 真实星野(银河) + ACES调色 + 泛光
|
|
撕裂伤口 + 碎石量级谱 + 尘埃云
|
|
模块加载(import map), 模式: journey / orbit
|
|
============================================================ */
|
|
|
|
import * as THREE from "three";
|
|
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
|
|
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
|
|
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
|
|
|
|
if (!window.SPACE_MODE) {
|
|
window.SPACE_MODE = document.body.getAttribute("data-space") || "journey";
|
|
}
|
|
|
|
(function () {
|
|
"use strict";
|
|
|
|
var isMobile = window.innerWidth < 768 || "ontouchstart" in window;
|
|
var reducedMotion = window.matchMedia("(prefers-reduced-motion: reduced)").matches;
|
|
var bootEl = document.getElementById("spaceBoot");
|
|
var canvasHost = document.getElementById("spaceCanvas");
|
|
var isJourney = window.SPACE_MODE === "journey";
|
|
|
|
function fallback() {
|
|
if (bootEl) bootEl.remove();
|
|
document.body.classList.add("space-fallback", "space-ready");
|
|
}
|
|
|
|
var testC = document.createElement("canvas");
|
|
if (!(testC.getContext("webgl") || testC.getContext("experimental-webgl"))) { fallback(); return; }
|
|
|
|
/* ============================================================
|
|
工具
|
|
============================================================ */
|
|
function glowTexture(inner, mid, size) {
|
|
var c = document.createElement("canvas");
|
|
c.width = c.height = size;
|
|
var x = c.getContext("2d");
|
|
var g = x.createRadialGradient(size/2, size/2, 0, size/2, size/2, size/2);
|
|
g.addColorStop(0, inner);
|
|
g.addColorStop(0.25, inner.replace(/[\d.]+\)$/, "0.5)"));
|
|
g.addColorStop(0.55, mid.replace(/[\d.]+\)$/, "0.18)"));
|
|
g.addColorStop(0.8, mid.replace(/[\d.]+\)$/, "0.05)"));
|
|
g.addColorStop(1, "rgba(0,0,0,0)");
|
|
x.fillStyle = g;
|
|
x.fillRect(0, 0, size, size);
|
|
return new THREE.CanvasTexture(c);
|
|
}
|
|
|
|
// Three.js SphereGeometry 的标准 UV 映射
|
|
function uvToSphere(u, v, r) {
|
|
var phi = 2 * Math.PI * u;
|
|
var theta = Math.PI * v;
|
|
return new THREE.Vector3(
|
|
-r * Math.cos(phi) * Math.sin(theta),
|
|
r * Math.cos(theta),
|
|
r * Math.sin(phi) * Math.sin(theta)
|
|
);
|
|
}
|
|
|
|
// 冲击点: 朝向 +x+z (首页相机可见侧)
|
|
var IMPACT = { u: 0.334, v: 0.47 };
|
|
|
|
/* ============================================================
|
|
场景与渲染器(ACES电影调色)
|
|
============================================================ */
|
|
var scene = new THREE.Scene();
|
|
var camera = new THREE.PerspectiveCamera(55, innerWidth / innerHeight, 0.1, 3000);
|
|
var rig = new THREE.Group();
|
|
rig.add(camera);
|
|
scene.add(rig);
|
|
|
|
var renderer;
|
|
try {
|
|
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: "high-performance" });
|
|
} catch (e) { fallback(); return; }
|
|
renderer.setPixelRatio(Math.min(devicePixelRatio, isMobile ? 1.2 : 1.6));
|
|
renderer.setSize(innerWidth, innerHeight);
|
|
renderer.toneMapping = THREE.ACESFilmicToneMapping; // 电影级色调映射
|
|
renderer.toneMappingExposure = 1.15;
|
|
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
canvasHost.appendChild(renderer.domElement);
|
|
|
|
/* ---------- 泛光(只有高亮物体发光: 太阳/裂缝/代码粒子) ---------- */
|
|
var composer = null, bloomPass = null;
|
|
if (!isMobile) {
|
|
composer = new EffectComposer(renderer);
|
|
composer.addPass(new RenderPass(scene, camera));
|
|
bloomPass = new UnrealBloomPass(new THREE.Vector2(innerWidth, innerHeight), 0.85, 0.55, 0.78);
|
|
composer.addPass(bloomPass);
|
|
}
|
|
|
|
/* ============================================================
|
|
资产加载(真实纹理)
|
|
============================================================ */
|
|
var texLoader = new THREE.TextureLoader();
|
|
var loadState = { moon: false, sky: false };
|
|
|
|
/* ---------- 真实星野天球(银河横贯) ---------- */
|
|
var skyMesh = null;
|
|
texLoader.load("images/starfield.jpg", function (tex) {
|
|
tex.colorSpace = THREE.SRGBColorSpace;
|
|
tex.mapping = THREE.EquirectangularReflectionMapping;
|
|
var mat = new THREE.MeshBasicMaterial({ map: tex, side: THREE.BackSide, depthWrite: false });
|
|
mat.fog = false;
|
|
skyMesh = new THREE.Mesh(new THREE.SphereGeometry(1200, 48, 32), mat);
|
|
skyMesh.rotation.y = 2.2; // 让银河带处于好看的方位
|
|
skyMesh.rotation.z = 0.12;
|
|
scene.add(skyMesh);
|
|
loadState.sky = true;
|
|
}, undefined, function () { loadState.sky = true; });
|
|
|
|
/* ---------- 近景尘埃(保留视差最快层) ---------- */
|
|
function makeDustLayer(count, spread) {
|
|
var geo = new THREE.BufferGeometry();
|
|
var pos = new Float32Array(count * 3);
|
|
var col = new Float32Array(count * 3);
|
|
for (var i = 0; i < count; i++) {
|
|
pos[i*3] = (Math.random()-0.5) * spread;
|
|
pos[i*3+1] = (Math.random()-0.5) * spread;
|
|
pos[i*3+2] = (Math.random()-0.5) * spread;
|
|
var b = 0.5 + Math.random() * 0.5;
|
|
col[i*3] = b * 0.85; col[i*3+1] = b * 0.95; col[i*3+2] = b;
|
|
}
|
|
geo.setAttribute("position", new THREE.BufferAttribute(pos, 3));
|
|
geo.setAttribute("color", new THREE.BufferAttribute(col, 3));
|
|
var mat = new THREE.PointsMaterial({
|
|
size: 1.1, vertexColors: true, transparent: true, opacity: 0.55,
|
|
sizeAttenuation: true, depthWrite: false
|
|
});
|
|
return new THREE.Points(geo, mat);
|
|
}
|
|
var nearDust = makeDustLayer(isMobile ? 100 : 220, 200);
|
|
rig.add(nearDust);
|
|
|
|
/* ---------- 太阳: 小而烈 + 泛光 + 横向光纹 ---------- */
|
|
var sunGroup = new THREE.Group();
|
|
// 亮度>1的核心,推过泛光阈值
|
|
var sunCore = new THREE.Mesh(
|
|
new THREE.SphereGeometry(5, 24, 24),
|
|
new THREE.MeshBasicMaterial({ color: new THREE.Color(2.6, 2.3, 1.9) })
|
|
);
|
|
sunGroup.add(sunCore);
|
|
// 柔和光晕(克制)
|
|
var sunHalo = new THREE.Sprite(new THREE.SpriteMaterial({
|
|
map: glowTexture("rgba(255,232,190,0.85)", "rgba(255,170,90,0.6)"),
|
|
transparent: true, opacity: 0.4, blending: THREE.AdditiveBlending, depthWrite: false
|
|
}));
|
|
sunHalo.scale.set(46, 46, 1);
|
|
sunGroup.add(sunHalo);
|
|
// 变形宽银幕横向光纹
|
|
var streakC = document.createElement("canvas");
|
|
streakC.width = 256; streakC.height = 8;
|
|
var sx = streakC.getContext("2d");
|
|
var sg = sx.createLinearGradient(0, 0, 256, 0);
|
|
sg.addColorStop(0, "rgba(255,215,160,0)");
|
|
sg.addColorStop(0.5, "rgba(255,235,200,0.85)");
|
|
sg.addColorStop(1, "rgba(255,215,160,0)");
|
|
sx.fillStyle = sg; sx.fillRect(0, 0, 256, 8);
|
|
var streakTex = new THREE.CanvasTexture(streakC);
|
|
var sunStreak = new THREE.Sprite(new THREE.SpriteMaterial({
|
|
map: streakTex, transparent: true, opacity: 0.55,
|
|
blending: THREE.AdditiveBlending, depthWrite: false
|
|
}));
|
|
sunStreak.scale.set(90, 1.6, 1);
|
|
sunGroup.add(sunStreak);
|
|
sunGroup.position.set(-150, 70, -260);
|
|
scene.add(sunGroup);
|
|
|
|
/* ============================================================
|
|
月亮: 真实纹理 + 撕裂伤口 + 发光裂缝
|
|
============================================================ */
|
|
var moonGroup = new THREE.Group();
|
|
var MOON_R = 10;
|
|
var moonMat = new THREE.MeshStandardMaterial({
|
|
color: 0xbfc3c9, // 冷调青灰
|
|
roughness: 0.98, metalness: 0.0,
|
|
bumpScale: 0.6
|
|
});
|
|
var moon = null;
|
|
|
|
// 发光裂缝纹理(emissiveMap): 冲击点+放射裂纹+断面代码
|
|
function makeCrackEmissive() {
|
|
var W = 1024, H = 512;
|
|
var c = document.createElement("canvas");
|
|
c.width = W; c.height = H;
|
|
var x = c.getContext("2d");
|
|
x.fillStyle = "#000"; x.fillRect(0, 0, W, H);
|
|
var ix = IMPACT.u * W, iy = IMPACT.v * H;
|
|
|
|
// 断面发光代码结构
|
|
x.save();
|
|
x.beginPath(); x.arc(ix, iy, 58, 0, 7); x.clip();
|
|
x.strokeStyle = "rgba(90,255,195,0.5)"; x.lineWidth = 1;
|
|
for (var g = -58; g <= 58; g += 9) {
|
|
x.beginPath(); x.moveTo(ix + g, iy - 58); x.lineTo(ix + g, iy + 58); x.stroke();
|
|
x.beginPath(); x.moveTo(ix - 58, iy + g); x.lineTo(ix + 58, iy + g); x.stroke();
|
|
}
|
|
var chars = "01{}<>/=;#*";
|
|
x.font = "9px monospace"; x.textBaseline = "top";
|
|
for (var i = 0; i < 55; i++) {
|
|
x.fillStyle = "rgba(160,255,215," + (0.4 + Math.random() * 0.6) + ")";
|
|
x.fillText(chars[Math.floor(Math.random() * chars.length)], ix - 52 + Math.random() * 104, iy - 52 + Math.random() * 104);
|
|
}
|
|
x.restore();
|
|
|
|
// 冲击核心辉光
|
|
var core = x.createRadialGradient(ix, iy, 0, ix, iy, 80);
|
|
core.addColorStop(0, "rgba(200,255,230,1)");
|
|
core.addColorStop(0.3, "rgba(120,255,205,0.5)");
|
|
core.addColorStop(1, "rgba(80,230,175,0)");
|
|
x.fillStyle = core; x.beginPath(); x.arc(ix, iy, 80, 0, 7); x.fill();
|
|
|
|
// 放射裂纹(宽淡→窄亮)
|
|
x.lineCap = "round"; x.lineJoin = "round";
|
|
for (var c2 = 0; c2 < 6; c2++) {
|
|
var ang = (c2 / 6) * Math.PI * 2 + (Math.random() - 0.5) * 0.8;
|
|
var px = ix, py = iy;
|
|
var pts = [[px, py]];
|
|
var len = 0.16 + Math.random() * 0.2;
|
|
var steps = 6 + Math.floor(Math.random() * 4);
|
|
for (var s = 0; s < steps; s++) {
|
|
ang += (Math.random() - 0.5) * 0.8;
|
|
px += Math.cos(ang) * (len * W / steps);
|
|
py += Math.sin(ang) * (len * W / steps) * 0.55;
|
|
pts.push([px, py]);
|
|
}
|
|
[[7, "rgba(70,220,170,0.1)"], [3.5, "rgba(100,240,190,0.22)"], [1.4, "rgba(200,255,235,0.9)"]].forEach(function (ps) {
|
|
x.strokeStyle = ps[1]; x.lineWidth = ps[0];
|
|
x.beginPath(); x.moveTo(pts[0][0], pts[0][1]);
|
|
for (var k = 1; k < pts.length; k++) x.lineTo(pts[k][0], pts[k][1]);
|
|
x.stroke();
|
|
});
|
|
}
|
|
return new THREE.CanvasTexture(c);
|
|
}
|
|
|
|
// 顶点撕裂: 把伤口区域向内撕成参差轮廓
|
|
function tearGeometry(geo, woundDir) {
|
|
var pos = geo.attributes.position;
|
|
var v = new THREE.Vector3();
|
|
for (var i = 0; i < pos.count; i++) {
|
|
v.fromBufferAttribute(pos, i).normalize();
|
|
var ang = v.angleTo(woundDir);
|
|
if (ang < 0.45) {
|
|
var fall = 1 - ang / 0.45;
|
|
// 高频噪声 → 锯齿而非圆坑
|
|
var n = 0.5 + 0.5 * Math.sin(v.x * 13.1) * Math.sin(v.y * 9.7) * Math.sin(v.z * 11.3)
|
|
+ 0.25 * Math.sin(v.x * 31.7 + v.y * 23.9);
|
|
n = Math.max(0, Math.min(1, n));
|
|
var depth = fall * (0.4 + 2.8 * n * (0.35 + 0.65 * fall));
|
|
if (ang > 0.3) depth += (Math.random() - 0.5) * 0.4; // 撕裂缘参差
|
|
var newR = MOON_R - Math.max(0, depth);
|
|
pos.setXYZ(i, v.x * newR, v.y * newR, v.z * newR);
|
|
}
|
|
}
|
|
geo.computeVertexNormals();
|
|
}
|
|
|
|
var woundDirLocal = uvToSphere(IMPACT.u, IMPACT.v, 1).normalize();
|
|
var woundPoint = uvToSphere(IMPACT.u, IMPACT.v, MOON_R * 0.98);
|
|
|
|
texLoader.load("images/moon_color.jpg", function (colorTex) {
|
|
colorTex.colorSpace = THREE.SRGBColorSpace;
|
|
moonMat.map = colorTex;
|
|
moonMat.emissiveMap = makeCrackEmissive();
|
|
moonMat.emissive = new THREE.Color(0x9fffd8);
|
|
moonMat.emissiveIntensity = 2.1;
|
|
|
|
// 亮度转凹凸(NASA位移图下载失败时的运行时方案,效果扎实)
|
|
var img = colorTex.image;
|
|
var bc = document.createElement("canvas");
|
|
bc.width = 1024; bc.height = 512;
|
|
var bx = bc.getContext("2d");
|
|
bx.drawImage(img, 0, 0, 1024, 512);
|
|
var id = bx.getImageData(0, 0, 1024, 512);
|
|
var d = id.data;
|
|
for (var i = 0; i < d.length; i += 4) {
|
|
var lum = d[i] * 0.299 + d[i+1] * 0.587 + d[i+2] * 0.114;
|
|
lum = Math.max(0, Math.min(255, (lum - 110) * 1.7 + 100));
|
|
d[i] = d[i+1] = d[i+2] = lum;
|
|
}
|
|
bx.putImageData(id, 0, 0);
|
|
moonMat.bumpMap = new THREE.CanvasTexture(bc);
|
|
|
|
var geo = new THREE.SphereGeometry(MOON_R, isMobile ? 64 : 96, isMobile ? 48 : 72);
|
|
tearGeometry(geo, woundDirLocal);
|
|
moon = new THREE.Mesh(geo, moonMat);
|
|
moonGroup.add(moon);
|
|
loadState.moon = true;
|
|
}, undefined, function () { loadState.moon = true; });
|
|
|
|
// 月缘柔光(青灰)
|
|
var moonHalo = new THREE.Sprite(new THREE.SpriteMaterial({
|
|
map: glowTexture("rgba(175,200,195,0.5)", "rgba(160,190,185,0.4)"),
|
|
transparent: true, opacity: 0.35, blending: THREE.AdditiveBlending, depthWrite: false
|
|
}));
|
|
moonHalo.scale.set(30, 30, 1);
|
|
moonGroup.add(moonHalo);
|
|
|
|
// 伤口溢出辉光(活着的事件)
|
|
var woundGlow = new THREE.Sprite(new THREE.SpriteMaterial({
|
|
map: glowTexture("rgba(160,255,220,0.9)", "rgba(90,255,195,0.55)"),
|
|
transparent: true, opacity: 0.8, blending: THREE.AdditiveBlending, depthWrite: false
|
|
}));
|
|
woundGlow.scale.set(11, 11, 1);
|
|
woundGlow.position.copy(woundPoint).multiplyScalar(1.04);
|
|
moonGroup.add(woundGlow);
|
|
|
|
/* ---------- 大碎片(2块,顶点扰动,真月面) ---------- */
|
|
var fragments = [];
|
|
var fragSpecs = [
|
|
{ size: 2.4, scatter: new THREE.Vector3(8.5, 4.5, 4), home: new THREE.Vector3(6.8, 3.6, 2.8), spin: 0.16, period: 14 },
|
|
{ size: 1.6, scatter: new THREE.Vector3(-6.5, -5, 5), home: new THREE.Vector3(-5, -3.8, 3.8), spin: -0.24, period: 9 }
|
|
];
|
|
fragSpecs.forEach(function (spec) {
|
|
var geo = new THREE.IcosahedronGeometry(spec.size, 2);
|
|
var pa = geo.getAttribute("position");
|
|
var vv = new THREE.Vector3();
|
|
for (var i = 0; i < pa.count; i++) {
|
|
vv.fromBufferAttribute(pa, i).normalize();
|
|
var j = 0.7 + Math.random() * 0.55;
|
|
pa.setXYZ(i, vv.x * spec.size * j, vv.y * spec.size * j, vv.z * spec.size * j);
|
|
}
|
|
geo.computeVertexNormals();
|
|
var mesh = new THREE.Mesh(geo, moonMat);
|
|
mesh.position.copy(spec.scatter);
|
|
mesh.rotation.set(Math.random() * 3, Math.random() * 3, Math.random() * 3);
|
|
moonGroup.add(mesh);
|
|
fragments.push({ mesh: mesh, spec: spec, phase: Math.random() * Math.PI * 2 });
|
|
});
|
|
|
|
/* ---------- 碎石量级谱(InstancedMesh, 从伤口拖出) ---------- */
|
|
var DEBRIS_N = isMobile ? 90 : 220;
|
|
var debrisData = [];
|
|
var debrisMesh = null;
|
|
(function buildDebris() {
|
|
var rockGeo = new THREE.IcosahedronGeometry(1, 0);
|
|
var rp = rockGeo.getAttribute("position");
|
|
for (var i = 0; i < rp.count; i++) {
|
|
var j = 0.65 + Math.random() * 0.7;
|
|
rp.setXYZ(i, rp.getX(i) * j, rp.getY(i) * j, rp.getZ(i) * j);
|
|
}
|
|
rockGeo.computeVertexNormals();
|
|
debrisMesh = new THREE.InstancedMesh(rockGeo, moonMat, DEBRIS_N);
|
|
var tangent = new THREE.Vector3().crossVectors(woundDirLocal, new THREE.Vector3(0, 1, 0)).normalize();
|
|
var bitan = new THREE.Vector3().crossVectors(woundDirLocal, tangent).normalize();
|
|
var dummy = new THREE.Object3D();
|
|
for (var d = 0; d < DEBRIS_N; d++) {
|
|
var along = (Math.random() - 0.3) * 30; // 沿切线拖带
|
|
var out = 1.5 + Math.random() * 9; // 向外飞散距离
|
|
var pos = woundPoint.clone()
|
|
.add(tangent.clone().multiplyScalar(along))
|
|
.add(woundDirLocal.clone().multiplyScalar(out))
|
|
.add(bitan.clone().multiplyScalar((Math.random() - 0.5) * 5));
|
|
var scale = 0.06 + Math.pow(Math.random(), 2.2) * 0.55; // 大量小颗,少数大颗
|
|
var vel = woundDirLocal.clone().multiplyScalar(0.1 + Math.random() * 0.25)
|
|
.add(tangent.clone().multiplyScalar((Math.random() - 0.5) * 0.1));
|
|
var axis = new THREE.Vector3(Math.random()-0.5, Math.random()-0.5, Math.random()-0.5).normalize();
|
|
debrisData.push({
|
|
pos: pos, scale: scale, vel: vel, axis: axis,
|
|
rotSpeed: (Math.random() - 0.5) * 0.6, phase: Math.random() * Math.PI * 2,
|
|
wobble: 0.3 + Math.random() * 0.7, period: 8 + Math.random() * 12
|
|
});
|
|
dummy.position.copy(pos);
|
|
dummy.scale.setScalar(scale);
|
|
dummy.updateMatrix();
|
|
debrisMesh.setMatrixAt(d, dummy.matrix);
|
|
}
|
|
debrisMesh.instanceMatrix.needsUpdate = true;
|
|
moonGroup.add(debrisMesh);
|
|
})();
|
|
|
|
/* ---------- 尘埃云(烟尘,参考图灵魂) ---------- */
|
|
var dustClouds = [];
|
|
var tangent2 = new THREE.Vector3().crossVectors(woundDirLocal, new THREE.Vector3(0, 1, 0)).normalize();
|
|
for (var dc = 0; dc < (isMobile ? 3 : 5); dc++) {
|
|
var dustTex = glowTexture("rgba(150,175,165,0.55)", "rgba(140,170,160,0.35)", 256);
|
|
var dust = new THREE.Sprite(new THREE.SpriteMaterial({
|
|
map: dustTex, transparent: true, opacity: 0.05 + Math.random() * 0.06,
|
|
blending: THREE.NormalBlending, depthWrite: false
|
|
}));
|
|
var sc2 = 22 + Math.random() * 26;
|
|
dust.scale.set(sc2 * 1.6, sc2, 1);
|
|
var along2 = 4 + dc * 7 + Math.random() * 4;
|
|
dust.position.copy(woundPoint)
|
|
.add(tangent2.clone().multiplyScalar(along2))
|
|
.add(woundDirLocal.clone().multiplyScalar(2 + Math.random() * 4));
|
|
moonGroup.add(dust);
|
|
dustClouds.push({ sp: dust, base: dust.position.clone(), phase: Math.random() * Math.PI * 2, along: along2 });
|
|
}
|
|
|
|
// 修复光缝
|
|
var seams = [];
|
|
var seamTex = glowTexture("rgba(160,255,220,1)", "rgba(90,255,195,0.6)", 64);
|
|
fragSpecs.forEach(function (spec) {
|
|
var seam = new THREE.Sprite(new THREE.SpriteMaterial({
|
|
map: seamTex, color: 0x9fffd8, transparent: true, opacity: 0,
|
|
blending: THREE.AdditiveBlending, depthWrite: false
|
|
}));
|
|
seam.scale.set(spec.size * 3.2, spec.size * 1.2, 1);
|
|
seam.position.copy(spec.home).multiplyScalar(0.94);
|
|
moonGroup.add(seam);
|
|
seams.push(seam);
|
|
});
|
|
|
|
moonGroup.position.set(6, 1, -26);
|
|
scene.add(moonGroup);
|
|
|
|
/* ---------- 代码泄漏粒子 ---------- */
|
|
var codeChars = "01{}<>/=;#*$fx";
|
|
var charCache = {};
|
|
function charTex(ch) {
|
|
if (!charCache[ch]) {
|
|
var s = 48, c = document.createElement("canvas");
|
|
c.width = c.height = s;
|
|
var x = c.getContext("2d");
|
|
x.font = "bold 30px monospace"; x.textAlign = "center"; x.textBaseline = "middle";
|
|
x.shadowColor = "#9fffd8"; x.shadowBlur = 9; x.fillStyle = "#d6ffee";
|
|
x.fillText(ch, s / 2, s / 2);
|
|
charCache[ch] = new THREE.CanvasTexture(c);
|
|
}
|
|
return charCache[ch];
|
|
}
|
|
var LEAK_N = isMobile ? 20 : 40;
|
|
var leaks = [];
|
|
var leakBase = woundPoint.clone().add(moonGroup.position);
|
|
for (var li = 0; li < LEAK_N; li++) {
|
|
var sp = new THREE.Sprite(new THREE.SpriteMaterial({
|
|
map: charTex(codeChars[Math.floor(Math.random() * codeChars.length)]),
|
|
transparent: true, opacity: 0, depthWrite: false, blending: THREE.AdditiveBlending
|
|
}));
|
|
var sc3 = 0.4 + Math.random() * 0.55;
|
|
sp.scale.set(sc3, sc3, 1);
|
|
var emit = leakBase.clone().add(new THREE.Vector3((Math.random()-0.5)*3, (Math.random()-0.5)*3, (Math.random()-0.5)*3));
|
|
sp.position.copy(emit);
|
|
scene.add(sp);
|
|
leaks.push({
|
|
sp: sp, life: Math.random(), speed: 0.08 + Math.random() * 0.14,
|
|
drift: woundDirLocal.clone().multiplyScalar(0.4 + Math.random() * 0.5)
|
|
.add(new THREE.Vector3((Math.random()-0.5)*0.4, (Math.random()-0.5)*0.4, (Math.random()-0.5)*0.3)),
|
|
origin: emit
|
|
});
|
|
}
|
|
|
|
/* ---------- 光照(太阳为唯一主光源) ---------- */
|
|
var sunLight = new THREE.DirectionalLight(0xffdcae, 2.6);
|
|
sunLight.position.set(-150, 70, -260);
|
|
scene.add(sunLight);
|
|
var fillCold = new THREE.DirectionalLight(0x5a6fc8, 0.4); // 背光面冷蓝紫
|
|
fillCold.position.set(120, -40, 120);
|
|
scene.add(fillCold);
|
|
scene.add(new THREE.AmbientLight(0x2a3452, 0.55));
|
|
var rimLight = new THREE.DirectionalLight(0x8899ff, 0.35);
|
|
rimLight.position.set(80, -40, 60);
|
|
scene.add(rimLight);
|
|
|
|
/* ============================================================
|
|
相机编排(V6行为: 呼吸漂移 + 鼠标惯性 + 滚动驱动)
|
|
============================================================ */
|
|
var mouseX = 0, mouseY = 0;
|
|
if (!isMobile) {
|
|
document.addEventListener("mousemove", function (e) {
|
|
mouseX = (e.clientX / innerWidth - 0.5) * 2;
|
|
mouseY = (e.clientY / innerHeight - 0.5) * 2;
|
|
});
|
|
}
|
|
function scrollProgress() {
|
|
var h = document.documentElement.scrollHeight - innerHeight;
|
|
return h > 0 ? Math.min(Math.max(scrollY / h, 0), 1) : 0;
|
|
}
|
|
function easeInOut(t) { return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; }
|
|
function lerp(a, b, t) { return a + (b - a) * t; }
|
|
|
|
var warpT = reducedMotion ? 1 : 0;
|
|
var clock = new THREE.Clock();
|
|
var bootDone = false;
|
|
|
|
/* ---------- BOOT ---------- */
|
|
var bootLines = [
|
|
{ t: "> 接入深空观测网络 ………… ", ok: "[OK]" },
|
|
{ t: "> 望远镜阵列对焦 …………… ", ok: "[OK]" },
|
|
{ t: "> 扫描目标: MOON.SYS ……… ", ok: "[FATAL]" },
|
|
{ t: "> 检测到结构性碎裂 ………… ", ok: "[FRAGS]" },
|
|
{ t: "> 启动修复协议 ……………… ", ok: "[GO]" }
|
|
];
|
|
var bootBody = document.getElementById("spaceBootBody");
|
|
var bootBar = document.getElementById("spaceBootBar");
|
|
function runBoot() {
|
|
if (!bootEl || reducedMotion) { finishBoot(); return; }
|
|
var i = 0;
|
|
(function next() {
|
|
if (i < bootLines.length) {
|
|
var line = document.createElement("div");
|
|
line.className = "terminal-line";
|
|
var isFatal = bootLines[i].ok.indexOf("FATAL") >= 0 || bootLines[i].ok.indexOf("FRAG") >= 0;
|
|
line.innerHTML = '<span class="terminal-prompt">' + bootLines[i].t + '</span><span class="terminal-text ' + (isFatal ? "log-error" : "ok") + '">' + bootLines[i].ok + "</span>";
|
|
if (bootBody) bootBody.appendChild(line);
|
|
if (bootBar) bootBar.style.width = ((i + 1) / bootLines.length * 100) + "%";
|
|
i++;
|
|
setTimeout(next, 340);
|
|
} else setTimeout(finishBoot, 420);
|
|
})();
|
|
}
|
|
function finishBoot() {
|
|
if (bootEl) {
|
|
bootEl.classList.add("hide");
|
|
setTimeout(function () { if (bootEl.parentNode) bootEl.parentNode.removeChild(bootEl); }, 900);
|
|
}
|
|
bootDone = true;
|
|
document.body.classList.add("space-ready");
|
|
}
|
|
runBoot();
|
|
setTimeout(finishBoot, 4500);
|
|
|
|
/* ============================================================
|
|
主循环
|
|
============================================================ */
|
|
var dummy = new THREE.Object3D();
|
|
var tmpQ = new THREE.Quaternion();
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
var t = clock.getElapsedTime();
|
|
var p = isJourney ? scrollProgress() : 0;
|
|
var ep = easeInOut(p);
|
|
|
|
// warp入场
|
|
if (warpT < 1) {
|
|
warpT = Math.min(1, warpT + 0.008);
|
|
var w = 1 - Math.pow(1 - warpT, 3);
|
|
camera.position.z = lerp(220, isJourney ? 46 : 42, w);
|
|
if (skyMesh) skyMesh.rotation.y += 0.004 * (1 - warpT);
|
|
} else if (isJourney) {
|
|
camera.position.z = lerp(46, 24, ep);
|
|
camera.position.y = lerp(0, 3, ep);
|
|
} else {
|
|
camera.position.z = 42;
|
|
}
|
|
|
|
// 呼吸式失重漂移
|
|
rig.position.x = Math.sin(t * 0.24) * 0.55;
|
|
rig.position.y = Math.cos(t * 0.19) * 0.45;
|
|
rig.rotation.z = Math.sin(t * 0.1) * 0.008;
|
|
|
|
// 鼠标惯性跟随
|
|
camera.rotation.y += ((-mouseX * 0.09) - camera.rotation.y) * 0.045;
|
|
camera.rotation.x += ((-mouseY * 0.06) - camera.rotation.x) * 0.045;
|
|
|
|
// 月亮缓慢自转 + 呼吸
|
|
if (moon) moon.rotation.y += 0.0006;
|
|
moonGroup.position.y = 1 + Math.sin(t * 0.33) * 0.3;
|
|
moonGroup.rotation.z = Math.sin(t * 0.11) * 0.015;
|
|
|
|
// 伤口辉光脉动
|
|
woundGlow.material.opacity = 0.65 + Math.sin(t * 1.6) * 0.2;
|
|
|
|
// 修复进度(V6时序)
|
|
var repair = isJourney ? Math.min(Math.max((p - 0.42) / 0.46, 0), 1) * 0.85 : 0.3;
|
|
var rt = repair / 0.85;
|
|
|
|
// 大碎片: 无衰减漂移,各自周期相位
|
|
fragments.forEach(function (f) {
|
|
var target = f.spec.scatter.clone().lerp(f.spec.home, rt * rt);
|
|
var amp = 0.5 * (1 - rt);
|
|
var w1 = (t / f.spec.period) * Math.PI * 2 + f.phase;
|
|
f.mesh.position.set(
|
|
target.x + Math.sin(w1) * amp,
|
|
target.y + Math.sin(w1 * 0.83 + 1.2) * amp * 0.8,
|
|
target.z + Math.cos(w1 * 0.71) * amp * 0.7
|
|
);
|
|
f.mesh.rotation.x += 0.0016 * f.spec.spin * (1 - rt * 0.8);
|
|
f.mesh.rotation.y += 0.0022 * f.spec.spin * (1 - rt * 0.8);
|
|
});
|
|
|
|
// 碎石场: 各自向外漂移+翻滚+不同步摆动(持续离散中)
|
|
if (debrisMesh) {
|
|
for (var d = 0; d < DEBRIS_N; d++) {
|
|
var dd = debrisData[d];
|
|
dd.pos.add(dd.vel.clone().multiplyScalar(0.016 * (1 - rt * 0.6)));
|
|
var wob = (t / dd.period) * Math.PI * 2 + dd.phase;
|
|
dummy.position.set(
|
|
dd.pos.x + Math.sin(wob) * dd.wobble * 0.3,
|
|
dd.pos.y + Math.sin(wob * 0.83) * dd.wobble * 0.25,
|
|
dd.pos.z + Math.cos(wob * 0.71) * dd.wobble * 0.25
|
|
);
|
|
tmpQ.setFromAxisAngle(dd.axis, t * dd.rotSpeed + dd.phase);
|
|
dummy.quaternion.copy(tmpQ);
|
|
dummy.scale.setScalar(dd.scale);
|
|
dummy.updateMatrix();
|
|
debrisMesh.setMatrixAt(d, dummy.matrix);
|
|
}
|
|
debrisMesh.instanceMatrix.needsUpdate = true;
|
|
}
|
|
|
|
// 尘埃云缓慢流动
|
|
dustClouds.forEach(function (dc2, i) {
|
|
dc2.sp.position.x = dc2.base.x + Math.sin(t * 0.1 + dc2.phase) * 1.2 + t * 0.05 * (i % 2 ? 1 : -0.7);
|
|
dc2.sp.position.y = dc2.base.y + Math.cos(t * 0.08 + dc2.phase) * 0.9;
|
|
dc2.sp.material.opacity = 0.05 + 0.03 * Math.sin(t * 0.23 + dc2.phase) + 0.04;
|
|
});
|
|
|
|
// 光缝
|
|
seams.forEach(function (s, i) {
|
|
s.material.opacity = rt * 0.9 + Math.sin(t * 2.2 + i * 1.7) * 0.06 * rt;
|
|
});
|
|
|
|
// 代码泄漏
|
|
var leakRate = 1 - rt * 0.55;
|
|
leaks.forEach(function (L) {
|
|
L.life += L.speed * 0.016;
|
|
if (L.life > 1) {
|
|
L.life = 0;
|
|
L.sp.position.copy(L.origin);
|
|
L.sp.material.map = charTex(codeChars[Math.floor(Math.random() * codeChars.length)]);
|
|
}
|
|
L.sp.position.add(L.drift.clone().multiplyScalar(0.016));
|
|
L.sp.material.opacity = Math.sin(L.life * Math.PI) * 0.8 * leakRate;
|
|
});
|
|
|
|
// 太阳呼吸 + 光纹微闪
|
|
sunHalo.material.opacity = 0.36 + Math.sin(t * 0.7) * 0.06;
|
|
sunStreak.material.opacity = 0.45 + Math.sin(t * 1.1) * 0.12;
|
|
|
|
// 星野极缓旋转 + 近尘埃视差
|
|
if (skyMesh) skyMesh.rotation.y += 0.00008;
|
|
nearDust.rotation.y += 0.0003;
|
|
|
|
if (composer) composer.render();
|
|
else renderer.render(scene, camera);
|
|
}
|
|
animate();
|
|
|
|
addEventListener("resize", function () {
|
|
camera.aspect = innerWidth / innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(innerWidth, innerHeight);
|
|
if (composer) composer.setSize(innerWidth, innerHeight);
|
|
});
|
|
})();
|