diff --git a/src/constants/config.ts b/src/constants/config.ts index 725d300..58bb330 100644 --- a/src/constants/config.ts +++ b/src/constants/config.ts @@ -1,7 +1,7 @@ export const APP_CONFIG = { // Rendering and canvas constraints. scene: { - background: '#030712', + background: '#000000', maxPixelRatio: 1.75, }, // Default perspective camera setup. @@ -13,13 +13,13 @@ export const APP_CONFIG = { }, // Shared motion timing and presets for the travel sequence. animation: { - speedMin: 0.1, + speedMin: 0.05, speedMax: 3, - defaultSpeed: 1, + defaultSpeed: 0.55, speedPresets: { - slow: 0.55, - steady: 1, - deep: 1.65, + slow: 0.2, + steady: 0.55, + deep: 1.0, }, }, // Starfield behavior tuned for center-area spawning and parallax depth. @@ -35,20 +35,20 @@ export const APP_CONFIG = { seedProgressExponent: 0.72, baseSizeMin: 1.3, baseSizeMax: 3.6, - baseAlphaMin: 0.62, - baseAlphaMax: 1, + baseAlphaMin: 2.0, + baseAlphaMax: 2.5, pointScale: 52, pointMinSize: 2.8, pointMaxSize: 76, accelerationXY: 0.22, accelerationZ: 0.1, - nearFieldBoost: 1.8, - edgeBoost: 7.2, - edgeBurst: 3.4, - trailStartProgress: 0.58, - trailStrength: 1, - trailLength: 0.38, - trailWidth: 0.16, + nearFieldBoost: 2.25, + edgeBoost: 8.8, + edgeBurst: 4.4, + trailStartProgress: 0.38, + trailStrength: 1.8, + trailLength: 0.85, + trailWidth: 0.12, }, // Smoke layers create volumetric light fog passing through the camera. smoke: { @@ -64,19 +64,113 @@ export const APP_CONFIG = { pointerSmoothing: 0.08, maxOffset: 0.4, }, - // Galaxy parameters for background spiral galaxy layer. + // Background galaxy rendered as layered shader fog instead of dense point clouds. galaxy: { - count: 50000, - radius: 15, - branches: 20, - spin: 0.3, - randomness: 0.15, - randomnessPower: 6, - size: 0.008, - rotationSpeed: 0.001, - zPosition: -35, - insideColor: '#ff6030', - outsideColor: '#3861cbff', + zPosition: -40, + width: 72, + height: 48, + tiltX: Math.PI * 0.35, + tiltY: Math.PI * 0.15, + palette: { + coreWhite: '#ffffff', + coreGold: '#ffd36b', + warmOrange: '#ff9850', + warmMagenta: '#7b4a86', + outerBlue: '#38d9ff', + deepBlue: '#103f97', + }, + layers: [ + { + scale: 1.05, + opacity: 0.42, + blendMode: 'normal', + rotationSpeed: 0.0024, + armCount: 2, + twist: 12, + armSharpness: 0.6, + noiseScale: 2.7, + noiseStrength: 0.54, + coreRadius: 0.2, + coreGlow: 3.8, + glowStrength: 1.06, + zOffset: 0, + innerColor: '#fff5de', + outerColor: '#63d8ff', + dustColor: '#1e3e78', + }, + { + scale: 1.25, + opacity: 0.38, + blendMode: 'normal', + rotationSpeed: 0.0016, + armCount: 3, + twist: 10.4, + armSharpness: 0.52, + noiseScale: 2.1, + noiseStrength: 0.68, + coreRadius: 0.24, + coreGlow: 3.5, + glowStrength: 0.82, + zOffset: -0.4, + innerColor: '#ffd8c2', + outerColor: '#6c89ff', + dustColor: '#45206b', + }, + { + scale: 1.45, + opacity: 0.52, + blendMode: 'additive', + rotationSpeed: 0.0009, + armCount: 4, + twist: 9.2, + armSharpness: 0.58, + noiseScale: 1.55, + noiseStrength: 0.76, + coreRadius: 0.28, + coreGlow: 2.8, + glowStrength: 1.08, + zOffset: -0.8, + innerColor: '#d8f4ff', + outerColor: '#25c7ef', + dustColor: '#0c2558', + }, + { + scale: 1.75, + opacity: 0.25, + blendMode: 'normal', + rotationSpeed: 0.00036, + armCount: 2, + twist: 6.8, + armSharpness: 0.28, + noiseScale: 1.05, + noiseStrength: 0.62, + coreRadius: 0.32, + coreGlow: 3.8, + glowStrength: 0.34, + zOffset: -1.3, + innerColor: '#b7e9ff', + outerColor: '#4ed5ef', + dustColor: '#10356b', + }, + { + scale: 2.1, + opacity: 0.22, + blendMode: 'normal', + rotationSpeed: 0.00018, + armCount: 2, + twist: 4.2, + armSharpness: 0.16, + noiseScale: 0.78, + noiseStrength: 0.56, + coreRadius: 0.42, + coreGlow: 3.9, + glowStrength: 0.2, + zOffset: -1.9, + innerColor: '#8fddff', + outerColor: '#53bfd8', + dustColor: '#143f73', + }, + ], }, // Shared UI timings used by overlays and toolbars. ui: { diff --git a/src/pages/HomePage.vue b/src/pages/HomePage.vue index 997dfa4..86a7f35 100644 --- a/src/pages/HomePage.vue +++ b/src/pages/HomePage.vue @@ -297,62 +297,70 @@ onBeforeUnmount(() => {
-
-
- 速度 -
+
+
+
+ 速度 +
+ +
+
+ + + + + +
+ 显示
- - - - - -
- 显示 - + +
diff --git a/src/styles/base.css b/src/styles/base.css index 0a093f0..96f8be7 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -13,7 +13,7 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - --bg-primary: #030712; + --bg-primary: #000000; --bg-secondary: rgba(7, 15, 30, 0.72); --border-soft: rgba(148, 163, 184, 0.24); --text-primary: #e2e8f0; @@ -37,9 +37,7 @@ body, body { min-height: 100vh; - background: - radial-gradient(circle at top, rgba(14, 165, 233, 0.18), transparent 32%), - linear-gradient(180deg, #020617 0%, #030712 45%, #02030a 100%); + background: #000000; color: var(--text-primary); } @@ -63,7 +61,7 @@ canvas { position: relative; min-height: 100vh; overflow: hidden; - background: radial-gradient(circle at top, rgba(56, 189, 248, 0.08), transparent 34%); + background: #000000; } .scene-layer { @@ -80,8 +78,7 @@ canvas { position: absolute; inset: 0; background: - radial-gradient(circle at center, transparent 0%, rgba(2, 6, 23, 0.08) 48%, rgba(2, 6, 23, 0.54) 100%), - linear-gradient(180deg, rgba(2, 6, 23, 0.16) 0%, rgba(2, 6, 23, 0.48) 100%); + radial-gradient(circle at center, transparent 30%, rgba(0, 0, 0, 0.4) 70%, rgba(0, 0, 0, 0.9) 100%); pointer-events: none; z-index: 1; } @@ -182,14 +179,23 @@ canvas { pointer-events: none; } -.glass-toolbar { +.hud-bottom-container { position: absolute; left: 50%; bottom: 28px; + width: min(860px, calc(100% - 24px)); + transform: translateX(-50%); + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; +} + +.glass-toolbar { display: flex; align-items: center; gap: 18px; - width: min(860px, calc(100% - 24px)); + width: 100%; padding: 18px; border: 1px solid rgba(148, 163, 184, 0.22); border-radius: 28px; @@ -198,7 +204,23 @@ canvas { backdrop-filter: blur(20px); box-shadow: 0 18px 56px rgba(2, 6, 23, 0.48); pointer-events: auto; - transform: translateX(-50%); +} + +.footer-credit { + font-size: 12px; + color: rgba(226, 232, 240, 0.5); + pointer-events: auto; +} + +.footer-credit a { + color: rgba(226, 232, 240, 0.8); + text-decoration: none; + font-weight: 500; + transition: text-decoration 160ms ease; +} + +.footer-credit a:hover { + text-decoration: underline; } .toolbar-group { @@ -226,10 +248,20 @@ canvas { .segment-control { display: inline-flex; + align-items: center; + height: 46px; padding: 4px; border: 1px solid rgba(148, 163, 184, 0.16); border-radius: 999px; background: rgba(2, 6, 23, 0.34); + transition: + border-color 160ms ease, + background 160ms ease; +} + +.segment-control:hover { + border-color: rgba(125, 211, 252, 0.32); + background: rgba(15, 23, 42, 0.56); } .segment-button { @@ -238,7 +270,11 @@ canvas { background: transparent; color: rgba(226, 232, 240, 0.78); cursor: pointer; - padding: 10px 16px; + height: 100%; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 16px; transition: background 160ms ease, color 160ms ease; @@ -250,13 +286,17 @@ canvas { } .toolbar-action { + height: 46px; min-height: 46px; border: 1px solid rgba(148, 163, 184, 0.18); - border-radius: 16px; + border-radius: 999px; background: rgba(2, 6, 23, 0.36); color: var(--text-primary); cursor: pointer; padding: 0 16px; + display: inline-flex; + align-items: center; + justify-content: center; transition: border-color 160ms ease, background 160ms ease, @@ -288,17 +328,102 @@ canvas { .glass-select { border: 1px solid rgba(148, 163, 184, 0.18); - border-radius: 16px; - background: rgba(2, 6, 23, 0.36); + border-radius: 999px; + background-color: rgba(2, 6, 23, 0.36); + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='rgba(226, 232, 240, 0.82)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 14px center; + background-size: 16px; + -webkit-appearance: none; + appearance: none; color: var(--text-primary); + height: 46px; min-height: 46px; outline: none; - padding: 0 14px; + padding: 0 36px 0 16px; + transition: + border-color 160ms ease, + background-color 160ms ease; +} + +.glass-select:hover { + border-color: rgba(125, 211, 252, 0.32); + background-color: rgba(15, 23, 42, 0.56); +} + +.range-wrapper { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 46px; + border: 1px solid rgba(148, 163, 184, 0.18); + border-radius: 999px; + background: linear-gradient(to right, rgba(56, 189, 248, 0.16) var(--volume-percent, 0%), rgba(2, 6, 23, 0.36) var(--volume-percent, 0%)); + background-clip: padding-box; + transition: border-color 160ms ease; + overflow: hidden; +} + +.range-wrapper:hover { + border-color: rgba(125, 211, 252, 0.32); +} + +.range-value { + position: absolute; + pointer-events: none; + font-size: 14px; + color: rgba(226, 232, 240, 0.9); + font-weight: 500; + z-index: 1; } .glass-range { - accent-color: var(--accent); + -webkit-appearance: none; + appearance: none; + position: absolute; + inset: 0; + width: 100%; + height: 100%; + border: none; + background: transparent; + outline: none; cursor: pointer; + margin: 0; + padding: 0; +} + +.glass-range::-webkit-slider-runnable-track { + width: 100%; + height: 100%; + background: transparent; + border: none; +} + +.glass-range::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 0; + height: 0; + opacity: 0; + border: none; + background: transparent; +} + +.glass-range::-moz-range-track { + width: 100%; + height: 100%; + background: transparent; + border: none; +} + +.glass-range::-moz-range-thumb { + width: 0; + height: 0; + opacity: 0; + border: none; + background: transparent; } .overlay-fade-enter-active, @@ -340,12 +465,16 @@ canvas { font-size: 15px; } - .glass-toolbar { + .hud-bottom-container { bottom: 16px; + width: min(520px, calc(100% - 16px)); + gap: 12px; + } + + .glass-toolbar { flex-direction: column; align-items: stretch; gap: 14px; - width: min(520px, calc(100% - 16px)); border-radius: 24px; padding: 20px; } diff --git a/src/utils/createTravelScene.ts b/src/utils/createTravelScene.ts index 21d8896..585371e 100644 --- a/src/utils/createTravelScene.ts +++ b/src/utils/createTravelScene.ts @@ -61,7 +61,13 @@ const SMOKE_RESET_Z = APP_CONFIG.smoke.resetZ const SMOKE_BOUNDS_X = APP_CONFIG.smoke.boundsX const SMOKE_BOUNDS_Y = APP_CONFIG.smoke.boundsY -const STAR_COLORS = ['#ffffff', '#f3f8ff', '#e8f4ff', '#d7eeff', '#fff7ea'] as const +const STAR_COLORS = [ + '#00e5ff', // 亮青 + '#ff00ea', // 亮粉 + '#ffcc00', // 亮金 + '#00ff66', // 亮绿 + '#ff3333', // 亮红 +] as const const SMOKE_COLORS = ['#8dd8ff', '#adc4ff', '#d6b5ff', '#ffe7ff'] as const const STAR_VERTEX_SHADER = ` @@ -78,11 +84,13 @@ varying vec2 vDir; void main() { vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); float scaledProgress = max(aProgress, 0.0); - float pointSize = aSizeBase * (0.95 + scaledProgress * 2.85) * ${APP_CONFIG.starfield.pointScale.toFixed(1)} / max(1.0, -mvPosition.z); + // 放大精灵尺寸以容纳更长的拖尾 + float pointSize = aSizeBase * (0.95 + scaledProgress * 2.85) * ${APP_CONFIG.starfield.pointScale.toFixed(1)} / max(1.0, -mvPosition.z) * 2.5; gl_Position = projectionMatrix * mvPosition; - gl_PointSize = clamp(pointSize, ${APP_CONFIG.starfield.pointMinSize.toFixed(1)}, ${APP_CONFIG.starfield.pointMaxSize.toFixed(1)}); + gl_PointSize = clamp(pointSize, ${APP_CONFIG.starfield.pointMinSize.toFixed(1)} * 2.5, ${APP_CONFIG.starfield.pointMaxSize.toFixed(1)} * 2.5); vColor = color; - vAlpha = aAlphaBase * (0.32 + scaledProgress * 0.68); + // 取消原本透明度的距离衰减,直接用最高的 baseAlpha 甚至更高,保持全局最亮 + vAlpha = aAlphaBase * 1.5; vTail = aTail; vDir = aDir; } @@ -93,27 +101,246 @@ varying vec3 vColor; varying float vAlpha; varying float vTail; varying vec2 vDir; +uniform float uSpeed; void main() { - vec2 centered = gl_PointCoord - vec2(0.5); + // 放大坐标系,这样中心点画得相对较小,留出极大空间画长尾巴 + vec2 centered = (gl_PointCoord - vec2(0.5)) * 2.5; float distanceToCenter = length(centered); - float core = 1.0 - smoothstep(0.0, 0.22, distanceToCenter); - float halo = 1.0 - smoothstep(0.12, 0.5, distanceToCenter); - vec2 dir = length(vDir) > 0.0001 ? normalize(vDir) : vec2(0.0, 1.0); + float hotspot = 1.0 - smoothstep(0.0, 0.06, distanceToCenter); + hotspot = pow(hotspot, 0.32); + float core = 1.0 - smoothstep(0.0, 0.13, distanceToCenter); + core = pow(core, 0.44); + float halo = 1.0 - smoothstep(0.015, 0.22, distanceToCenter); + halo = pow(halo, 1.95); + + // 翻转 Y 轴以匹配 WebGL 纹理坐标系 + vec2 dir = length(vDir) > 0.0001 ? normalize(vec2(vDir.x, -vDir.y)) : vec2(0.0, -1.0); vec2 side = vec2(-dir.y, dir.x); float along = dot(centered, dir); float across = dot(centered, side); - float tailLong = 1.0 - smoothstep(-${(0.18 + APP_CONFIG.starfield.trailLength).toFixed(2)}, 0.05, along); - float tailThin = 1.0 - smoothstep(${(APP_CONFIG.starfield.trailWidth * 0.35).toFixed(2)}, ${(APP_CONFIG.starfield.trailWidth).toFixed(2)}, abs(across)); - float tailFade = smoothstep(0.01, 0.52, -along); - float tail = tailLong * tailThin * tailFade * vTail; - float alpha = (core * 0.95 + halo * 0.45 + tail * 0.85) * vAlpha; + + float back = -along; + // 拖尾长度根据当前速度动态变化 + float speedFactor = max(uSpeed, 0.1); + float tailLength = clamp(${(APP_CONFIG.starfield.trailLength).toFixed(2)} * 2.5 * speedFactor, 0.05, 2.5); + float tailFade = 1.0 - smoothstep(0.0, tailLength, back); + float tailMask = smoothstep(-0.02, 0.05, back); + + float maxWidth = ${(APP_CONFIG.starfield.trailWidth).toFixed(2)}; + float width = maxWidth * mix(0.1, 1.0, pow(tailFade, 0.8)); + + float tailProfile = 1.0 - smoothstep(0.0, width, abs(across)); + float tailHotspot = 1.0 - smoothstep(0.0, width * 0.3, abs(across)); + // 速度越快,尾巴亮度也适当增强 + float tail = (tailProfile * 0.8 + tailHotspot * 1.5) * tailFade * tailMask * vTail * (0.4 + 0.6 * speedFactor); + + // 极高的能量累加,不丢弃任何 alpha,强制发光 + float alpha = (hotspot * 3.5 + core * 2.5 + halo * 0.5 + tail * 2.8) * vAlpha; if (alpha < 0.01) { discard; } - gl_FragColor = vec4(vColor, alpha); + vec3 color = vColor; + // 让高光点保持自身颜色的同时极度明亮,而不仅是变白 + vec3 hotColor = mix(vColor, vec3(1.0), 0.5); + color = mix(color, hotColor, hotspot * 0.8 + core * 0.4); + color += hotColor * hotspot * 2.0; + color += vColor * core * 1.5; + color += vColor * tail * 1.8; + + // 用极其暴力的颜色叠加 + gl_FragColor = vec4(min(color, vec3(1.0)), min(alpha, 1.0)); +} +` + +const GALAXY_VERTEX_SHADER = ` +varying vec2 vUv; + +void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); +} +` + +const GALAXY_FRAGMENT_SHADER = ` +varying vec2 vUv; + +uniform float uTime; +uniform float uOpacity; +uniform float uArms; +uniform float uTwist; +uniform float uArmSharpness; +uniform float uNoiseScale; +uniform float uNoiseStrength; +uniform float uCoreRadius; +uniform float uCoreGlow; +uniform float uGlowStrength; +uniform float uSeed; +uniform vec3 uCoreWhite; +uniform vec3 uCoreGold; +uniform vec3 uWarmOrange; +uniform vec3 uWarmMagenta; +uniform vec3 uOuterBlue; +uniform vec3 uDeepBlue; +uniform vec3 uInnerColor; +uniform vec3 uOuterColor; +uniform vec3 uDustColor; + +float hash(vec2 p) { + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); +} + +float noise(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + vec2 u = f * f * (3.0 - 2.0 * f); + + return mix( + mix(hash(i + vec2(0.0, 0.0)), hash(i + vec2(1.0, 0.0)), u.x), + mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x), + u.y + ); +} + +float fbm(vec2 p) { + float value = 0.0; + float amplitude = 0.5; + + for (int i = 0; i < 5; i += 1) { + value += amplitude * noise(p); + p = p * 2.03 + vec2(17.3, 9.2); + amplitude *= 0.5; + } + + return value; +} + +vec3 sampleGalaxyRamp(float t) { + float value = clamp(t, 0.0, 1.0); + vec3 color = uCoreWhite; + + color = mix(color, uCoreGold, smoothstep(0.03, 0.18, value)); + color = mix(color, uWarmOrange, smoothstep(0.12, 0.32, value)); + color = mix(color, uWarmMagenta, smoothstep(0.24, 0.58, value)); + color = mix(color, uOuterBlue, smoothstep(0.5, 0.84, value)); + color = mix(color, uDeepBlue, smoothstep(0.76, 1.0, value)); + + return color; +} + +void main() { + vec2 p = vUv * 2.0 - 1.0; + p.x *= 1.08; + + float radius = length(p); + float angle = atan(p.y, p.x); + float time = uTime * 0.045; + vec2 flowUv = p * uNoiseScale; + + float lowNoise = fbm(flowUv + vec2(time * 0.35 + uSeed * 5.1, -time * 0.18)); + float highNoise = fbm(flowUv * 2.1 - vec2(time * 0.42, time * 0.26 + uSeed * 3.7)); + float veilNoise = fbm(p * (uNoiseScale * 0.55) + vec2(-time * 0.12, time * 0.08 + uSeed * 1.9)); + float filamentNoise = fbm(flowUv * 3.4 + vec2(time * 0.18 - uSeed * 2.7, time * 0.24)); + float clusterNoise = fbm(flowUv * 5.1 + vec2(-time * 0.32 + uSeed * 8.4, time * 0.21)); + float grainNoise = noise(flowUv * 13.5 + vec2(time * 1.8 + uSeed * 14.0, -time * 1.3)); + float fineNoise = fbm(flowUv * 8.2 - vec2(time * 0.65 - uSeed * 6.2, time * 0.52)); + float warpedAngle = angle - radius * uTwist + (lowNoise - 0.5) * uNoiseStrength * 3.2; + vec2 swirlDir = vec2(cos(warpedAngle), sin(warpedAngle)); + vec2 swirlUv = vec2(dot(p, swirlDir), dot(p, vec2(-swirlDir.y, swirlDir.x))); + float streakNoise = fbm(swirlUv * vec2(24.0, 3.2) + vec2(time * 0.12 + uSeed * 4.6, -time * 0.06)); + float mottledNoise = fbm(swirlUv * vec2(10.0, 6.5) - vec2(time * 0.08 - uSeed * 5.2, time * 0.04)); + float fineFiberNoise = fbm(swirlUv * vec2(46.0, 1.8) + vec2(time * 0.06 + uSeed * 11.4, -time * 0.03)); + float occlusionNoise = fbm(swirlUv * vec2(5.4, 4.1) - vec2(time * 0.04 - uSeed * 2.6, time * 0.02)); + float spiralWave = cos(warpedAngle * uArms); + float armBase = spiralWave * 0.5 + 0.5; + float armMask = smoothstep(1.0 - uArmSharpness, 1.0, armBase); + float softArms = smoothstep(0.2, 0.92, armBase + (lowNoise - 0.5) * 0.18); + float halo = 1.0 - smoothstep(0.12, 1.45, radius); + float outerHalo = 1.0 - smoothstep(0.4, 1.6, radius); + float core = 1.0 - smoothstep(0.0, uCoreRadius, radius); + float coreBloom = 1.0 - smoothstep(0.0, uCoreRadius * 1.6, radius); + float hotCore = 1.0 - smoothstep(0.0, uCoreRadius * 0.95, radius); + float dust = smoothstep(0.24, 0.9, highNoise); + float wisps = smoothstep(0.44, 0.9, lowNoise) * (1.0 - smoothstep(0.38, 1.08, radius)); + float outerVeil = smoothstep(0.34, 0.88, veilNoise) * outerHalo; + float rim = smoothstep(0.3, 0.98, radius) * (1.0 - smoothstep(0.92, 1.3, radius)); + float armRidge = pow(armMask, 1.45) * halo; + float lanePattern = 1.0 - smoothstep(0.2, 0.72, abs(sin(warpedAngle * (uArms * 0.55) + filamentNoise * 4.4))); + float dustLane = lanePattern * smoothstep(0.12, 0.88, radius) * halo * (0.35 + (1.0 - armMask) * 0.9); + float clumps = smoothstep(0.62, 0.9, filamentNoise) * softArms * rim; + float pulse = 0.5 + 0.5 * sin(uTime * 1.6 + uSeed * 9.0); + float shockRadius = uCoreRadius * (1.08 + pulse * 0.42); + float shockRing = smoothstep(shockRadius - 0.17, shockRadius - 0.04, radius) * + (1.0 - smoothstep(shockRadius + 0.03, shockRadius + 0.11, radius)) * + mix(0.55, 1.0, armMask); + float clusterMask = smoothstep(0.72, 0.9, clusterNoise) * pow(softArms, 1.45) * rim; + float clusterTwinkle = 0.55 + 0.45 * sin(uTime * 2.6 + angle * uArms * 3.2 + radius * 16.0 + uSeed * 12.0); + float armSpark = clusterMask * clusterTwinkle; + float edgeBreak = smoothstep(0.72, 0.95, veilNoise) * smoothstep(0.55, 1.08, radius) * outerHalo; + float grainMask = smoothstep(0.36, 0.84, grainNoise) * mix(0.32, 1.0, softArms) * halo; + float dustTexture = smoothstep(0.28, 0.78, fineNoise) * halo * (0.45 + (1.0 - armMask) * 0.8); + float fiberBands = smoothstep(0.48, 0.88, streakNoise) * halo * smoothstep(0.12, 0.92, radius); + float mottledDust = smoothstep(0.34, 0.82, mottledNoise) * halo; + float fineFibers = smoothstep(0.54, 0.9, fineFiberNoise) * halo * smoothstep(0.1, 0.96, radius); + float deepOcclusion = smoothstep(0.38, 0.8, occlusionNoise) * halo * smoothstep(0.18, 1.0, radius); + float dustFilaments = fiberBands * (0.4 + (1.0 - armMask) * 0.9) * (0.6 + dustLane * 0.8); + float outerSparseFibers = smoothstep(0.44, 0.88, streakNoise) * + smoothstep(0.48, 1.08, radius) * + outerHalo * + (0.45 + veilNoise * 0.8); + float microSpeckles = smoothstep(0.955, 0.992, grainNoise) * (0.18 + clusterTwinkle * 0.32) * softArms * rim; + float density = halo * mix(0.44, 1.0, armMask) * mix(0.88, 1.12, dust); + density = mix(density, halo * 0.96, 0.34); + float coreGlow = pow(core, uCoreGlow) * uGlowStrength; + float bloomGlow = pow(coreBloom, 1.8) * (uGlowStrength * 0.92); + float pulseCore = pow(coreBloom, 1.28) * (0.24 + pulse * 0.34); + float innerCloud = pow(core, 1.15) * 0.58 + pow(coreBloom, 1.3) * 0.38; + float starDust = smoothstep(0.9, 0.985, highNoise) * softArms * rim * 0.07; + + density += wisps * 0.4 + outerVeil * 0.42 + innerCloud + coreGlow + bloomGlow + starDust; + density += armRidge * 0.2 + clumps * 0.24 + edgeBreak * 0.18 + shockRing * 0.28 + armSpark * 0.1 + pulseCore; + density += grainMask * 0.04 + microSpeckles * 0.018; + density *= 1.0 - dustLane * 0.34; + density *= 0.94 + grainNoise * 0.11; + density *= 1.0 - dustTexture * 0.1; + density *= 1.0 - dustFilaments * 0.26; + density *= 1.0 - fineFibers * 0.18; + density *= 1.0 - outerSparseFibers * 0.13; + density *= 1.0 - deepOcclusion * (0.12 + dustLane * 0.1); + density *= 0.9 + (1.0 - mottledDust) * 0.12; + density *= exp(-radius * 0.85); + + float rampT = clamp(pow(radius, 0.72) + dustLane * 0.05 - coreBloom * 0.14 - armRidge * 0.03 + (1.0 - softArms) * 0.04, 0.0, 1.0); + vec3 rampColor = sampleGalaxyRamp(rampT); + vec3 layerTint = mix(uOuterColor, uInnerColor, clamp(core * 0.75 + armRidge * 0.24 + softArms * 0.12, 0.0, 1.0)); + vec3 color = mix(rampColor, layerTint, 0.25); // 增加 layerTint 的比重,提亮 + + // 提亮灰尘的颜色,防止在纯黑背景下太暗 + vec3 brightDust = mix(uDustColor, vec3(0.3, 0.5, 0.8), 0.3); + color = mix(color, brightDust, wisps * 0.12 + dustLane * 0.5 + (1.0 - softArms) * 0.1); + color = mix(color, brightDust, dustTexture * 0.15 + dustFilaments * 0.25 + fineFibers * 0.15 + mottledDust * 0.12 + deepOcclusion * 0.1); + color = mix(color, uWarmOrange, armRidge * 0.16 + shockRing * 0.1); + color = mix(color, uWarmMagenta, smoothstep(0.18, 0.62, radius) * (1.0 - armMask) * 0.24); + color = mix(color, uOuterBlue, outerVeil * 0.18 + edgeBreak * 0.12); + color = mix(color, mix(uOuterBlue, uDustColor, 0.4), outerSparseFibers * 0.36); + color += mix(rampColor, vec3(1.0, 0.98, 0.95), 0.4) * grainMask * 0.018; + color += mix(uCoreGold, vec3(1.0, 0.96, 0.86), 0.45) * armRidge * 0.18; + color += mix(uOuterBlue, vec3(0.9, 0.97, 1.0), 0.35) * clumps * 0.12; + color += mix(uCoreGold, uWarmOrange, 0.45) * shockRing * 0.34; + color += mix(uCoreWhite, uOuterBlue, 0.28) * armSpark * 0.12; + color += mix(uCoreWhite, uCoreGold, 0.4) * microSpeckles * 0.04; + color += uCoreWhite * pow(coreBloom, 1.8) * 0.65; + color += mix(uCoreWhite, uCoreGold, 0.22) * pow(hotCore, 1.2) * 1.0; + + float alpha = density * uOpacity * (1.0 - smoothstep(1.25, 1.6, radius)); + + if (alpha < 0.01) { + discard; + } + + gl_FragColor = vec4(color, clamp(alpha, 0.0, 1.0)); } ` @@ -134,6 +361,10 @@ function randomRange(min: number, max: number) { return min + Math.random() * (max - min) } +function getGalaxyBlending(blendMode: 'normal' | 'additive') { + return blendMode === 'additive' ? THREE.AdditiveBlending : THREE.NormalBlending +} + function sampleDisk(radius: number) { const angle = Math.random() * Math.PI * 2 const distance = Math.pow(Math.random(), APP_CONFIG.starfield.radialExponent) * radius @@ -236,14 +467,13 @@ export function createTravelScene(container: HTMLElement): TravelSceneController const galaxyGroup = new THREE.Group() galaxyGroup.position.z = APP_CONFIG.galaxy.zPosition - // Tilt the galaxy to give it a perspective view - galaxyGroup.rotation.x = Math.PI * 0.35 - galaxyGroup.rotation.y = Math.PI * 0.15 + galaxyGroup.rotation.x = APP_CONFIG.galaxy.tiltX + galaxyGroup.rotation.y = APP_CONFIG.galaxy.tiltY scene.add(galaxyGroup) - // Create an inner group for the galaxy rotation to preserve the tilt - const galaxyInnerGroup = new THREE.Group() - galaxyGroup.add(galaxyInnerGroup) + const galaxyGeometry = new THREE.PlaneGeometry(APP_CONFIG.galaxy.width, APP_CONFIG.galaxy.height, 1, 1) + const galaxyLayers: THREE.Mesh[] = [] + const galaxyMaterials: THREE.ShaderMaterial[] = [] const starsGeometry = new THREE.BufferGeometry() const starPositions = new Float32Array(STAR_COUNT * 3) @@ -264,7 +494,7 @@ export function createTravelScene(container: HTMLElement): TravelSceneController const pointerCurrent = { x: 0, y: 0 } let started = false - let targetSpeed = 1 + let targetSpeed: number = APP_CONFIG.animation.defaultSpeed let currentSpeed = 0 let animationFrameId = 0 let disposed = false @@ -276,6 +506,9 @@ export function createTravelScene(container: HTMLElement): TravelSceneController depthWrite: false, vertexColors: true, blending: THREE.AdditiveBlending, + uniforms: { + uSpeed: { value: 1.0 }, + }, }) function spawnStar(index: number, seededProgress?: number) { @@ -314,7 +547,11 @@ export function createTravelScene(container: HTMLElement): TravelSceneController starDirections[index * 2] = direction.x starDirections[index * 2 + 1] = direction.y - starColor.set(STAR_COLORS[Math.floor(Math.random() * STAR_COLORS.length)]) + if (Math.random() < 0.9) { + starColor.set('#ffffff') + } else { + starColor.set(STAR_COLORS[Math.floor(Math.random() * STAR_COLORS.length)]) + } starColors[index * 3] = starColor.r starColors[index * 3 + 1] = starColor.g starColors[index * 3 + 2] = starColor.b @@ -393,61 +630,47 @@ export function createTravelScene(container: HTMLElement): TravelSceneController travelRoot.add(state.mesh) } - const galaxyGeometry = new THREE.BufferGeometry() - const galaxyPositions = new Float32Array(APP_CONFIG.galaxy.count * 3) - const galaxyColors = new Float32Array(APP_CONFIG.galaxy.count * 3) - const colorInside = new THREE.Color(APP_CONFIG.galaxy.insideColor) - const colorOutside = new THREE.Color(APP_CONFIG.galaxy.outsideColor) + APP_CONFIG.galaxy.layers.forEach((layerConfig, index) => { + const galaxyMaterial = new THREE.ShaderMaterial({ + vertexShader: GALAXY_VERTEX_SHADER, + fragmentShader: GALAXY_FRAGMENT_SHADER, + transparent: true, + depthWrite: false, + side: THREE.DoubleSide, + blending: getGalaxyBlending(layerConfig.blendMode), + uniforms: { + uTime: { value: 0 }, + uOpacity: { value: layerConfig.opacity }, + uArms: { value: layerConfig.armCount }, + uTwist: { value: layerConfig.twist }, + uArmSharpness: { value: layerConfig.armSharpness }, + uNoiseScale: { value: layerConfig.noiseScale }, + uNoiseStrength: { value: layerConfig.noiseStrength }, + uCoreRadius: { value: layerConfig.coreRadius }, + uCoreGlow: { value: layerConfig.coreGlow }, + uGlowStrength: { value: layerConfig.glowStrength }, + uSeed: { value: index * 0.173 + 0.31 }, + uCoreWhite: { value: new THREE.Color(APP_CONFIG.galaxy.palette.coreWhite) }, + uCoreGold: { value: new THREE.Color(APP_CONFIG.galaxy.palette.coreGold) }, + uWarmOrange: { value: new THREE.Color(APP_CONFIG.galaxy.palette.warmOrange) }, + uWarmMagenta: { value: new THREE.Color(APP_CONFIG.galaxy.palette.warmMagenta) }, + uOuterBlue: { value: new THREE.Color(APP_CONFIG.galaxy.palette.outerBlue) }, + uDeepBlue: { value: new THREE.Color(APP_CONFIG.galaxy.palette.deepBlue) }, + uInnerColor: { value: new THREE.Color(layerConfig.innerColor) }, + uOuterColor: { value: new THREE.Color(layerConfig.outerColor) }, + uDustColor: { value: new THREE.Color(layerConfig.dustColor) }, + }, + }) - for (let i = 0; i < APP_CONFIG.galaxy.count; i += 1) { - const i3 = i * 3 - const radius = Math.random() * APP_CONFIG.galaxy.radius - const branchesAngle = - ((i % APP_CONFIG.galaxy.branches) / APP_CONFIG.galaxy.branches) * Math.PI * 2 - const spinAngle = radius * APP_CONFIG.galaxy.spin - - const mixedColor = colorInside.clone() - mixedColor.lerp(colorOutside, radius / APP_CONFIG.galaxy.radius) - - const randomX = - Math.random() ** APP_CONFIG.galaxy.randomnessPower * - (Math.random() < 0.5 ? 1 : -1) * - APP_CONFIG.galaxy.randomness * - radius - const randomY = - Math.random() ** APP_CONFIG.galaxy.randomnessPower * - (Math.random() < 0.5 ? 1 : -1) * - APP_CONFIG.galaxy.randomness * - radius - const randomZ = - Math.random() ** APP_CONFIG.galaxy.randomnessPower * - (Math.random() < 0.5 ? 1 : -1) * - APP_CONFIG.galaxy.randomness * - radius - - galaxyPositions[i3] = Math.cos(branchesAngle + spinAngle) * radius + randomX - galaxyPositions[i3 + 1] = Math.sin(branchesAngle + spinAngle) * radius + randomY - galaxyPositions[i3 + 2] = randomZ - - galaxyColors[i3] = mixedColor.r - galaxyColors[i3 + 1] = mixedColor.g - galaxyColors[i3 + 2] = mixedColor.b - } - - galaxyGeometry.setAttribute('position', new THREE.BufferAttribute(galaxyPositions, 3)) - galaxyGeometry.setAttribute('color', new THREE.BufferAttribute(galaxyColors, 3)) - - const galaxyMaterial = new THREE.PointsMaterial({ - size: APP_CONFIG.galaxy.size, - sizeAttenuation: true, - depthWrite: false, - blending: THREE.AdditiveBlending, - vertexColors: true, + const galaxyMesh = new THREE.Mesh(galaxyGeometry, galaxyMaterial) + galaxyMesh.scale.setScalar(layerConfig.scale) + galaxyMesh.position.z = layerConfig.zOffset + galaxyMesh.rotation.z = index * 0.8 + galaxyGroup.add(galaxyMesh) + galaxyLayers.push(galaxyMesh) + galaxyMaterials.push(galaxyMaterial) }) - const galaxyPoints = new THREE.Points(galaxyGeometry, galaxyMaterial) - galaxyInnerGroup.add(galaxyPoints) - const clock = new THREE.Clock() const onResize = () => { @@ -588,8 +811,15 @@ export function createTravelScene(container: HTMLElement): TravelSceneController const motionDelta = deltaSeconds * currentSpeed updateStars(motionDelta) updateSmoke(motionDelta * 0.62, elapsed) + + starMaterial.uniforms.uSpeed.value = currentSpeed - galaxyInnerGroup.rotation.z += APP_CONFIG.galaxy.rotationSpeed * currentSpeed + galaxyLayers.forEach((galaxyLayer, index) => { + galaxyLayer.rotation.z -= APP_CONFIG.galaxy.layers[index].rotationSpeed * currentSpeed + }) + galaxyMaterials.forEach((material, index) => { + material.uniforms.uTime.value = elapsed + index * 0.37 + }) travelRoot.rotation.z = pointerCurrent.x * 0.03 travelRoot.position.x = -pointerCurrent.x * 0.8 @@ -631,8 +861,8 @@ export function createTravelScene(container: HTMLElement): TravelSceneController travelRoot.remove(smokeState.mesh) } + galaxyMaterials.forEach((material) => material.dispose()) galaxyGeometry.dispose() - galaxyMaterial.dispose() starsGeometry.dispose() starMaterial.dispose()