当前位置: 首页 > news >正文

学习threejs,超炫银河黑洞效果模拟

👨‍⚕️ 主页: gis分享者
👨‍⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!
👨‍⚕️ 收录于专栏:threejs gis工程师


文章目录

  • 一、🍀前言
    • 1.1 ☘️THREE.EffectComposer 后期处理
      • 1.1.1 ☘️代码示例
      • 1.1.2 ☘️构造函数
      • 1.1.3 ☘️属性
      • 1.1.4 ☘️方法
    • 1.2 ☘️THREE.RenderPass
      • 1.2.1 ☘️构造函数
      • 1.2.2 ☘️属性
      • 1.2.3 ☘️方法
    • 1.3 ☘️THREE.UnrealBloomPass
      • 1.3.1 ☘️构造函数
      • 1.3.2 ☘️使用示例
      • 1.3.3 ☘️方法
  • 二、🍀超炫银河黑洞效果模拟
    • 1. ☘️实现思路
    • 2. ☘️代码样例


一、🍀前言

本文详细介绍如何基于threejs在三维场景中实现超炫银河黑洞效果模拟,亲测可用。希望能帮助到您。一起学习,加油!加油!

1.1 ☘️THREE.EffectComposer 后期处理

THREE.EffectComposer 用于在three.js中实现后期处理效果。该类管理了产生最终视觉效果的后期处理过程链。 后期处理过程根据它们添加/插入的顺序来执行,最后一个过程会被自动渲染到屏幕上。

1.1.1 ☘️代码示例

import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
// 初始化 composer
const composer = new EffectComposer(renderer);
// 创建 RenderPass 并添加到 composer
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
// 添加其他后期处理通道(如模糊)
// composer.addPass(blurPass);
// 在动画循环中渲染
function animate() {composer.render();requestAnimationFrame(animate);
}

1.1.2 ☘️构造函数

EffectComposer( renderer : WebGLRenderer, renderTarget : WebGLRenderTarget )
renderer – 用于渲染场景的渲染器。
renderTarget – (可选)一个预先配置的渲染目标,内部由 EffectComposer 使用。

1.1.3 ☘️属性

.passes : Array
一个用于表示后期处理过程链(包含顺序)的数组。

渲染通道:
BloomPass   该通道会使得明亮区域参入较暗的区域。模拟相机照到过多亮光的情形
DotScreenPass   将一层黑点贴到代表原始图片的屏幕上
FilmPass    通过扫描线和失真模拟电视屏幕
MaskPass    在当前图片上贴一层掩膜,后续通道只会影响被贴的区域
RenderPass  该通道在指定的场景和相机的基础上渲染出一个新的场景
SavePass    执行该通道时,它会将当前渲染步骤的结果复制一份,方便后面使用。这个通道实际应用中作用不大;
ShaderPass  使用该通道你可以传入一个自定义的着色器,用来生成高级的、自定义的后期处理通道
TexturePass 该通道可以将效果组合器的当前状态保存为一个纹理,然后可以在其他EffectCoposer对象中将该纹理作为输入参数

.readBuffer : WebGLRenderTarget
内部读缓冲区的引用。过程一般从该缓冲区读取先前的渲染结果。

.renderer : WebGLRenderer
内部渲染器的引用。

.renderToScreen : Boolean
最终过程是否被渲染到屏幕(默认帧缓冲区)。

.writeBuffer : WebGLRenderTarget
内部写缓冲区的引用。过程常将它们的渲染结果写入该缓冲区。

1.1.4 ☘️方法

.addPass ( pass : Pass ) : undefined
pass – 将被添加到过程链的过程

将传入的过程添加到过程链。

.dispose () : undefined
释放此实例分配的 GPU 相关资源。每当您的应用程序不再使用此实例时调用此方法。

.insertPass ( pass : Pass, index : Integer ) : undefined
pass – 将被插入到过程链的过程。

index – 定义过程链中过程应插入的位置。

将传入的过程插入到过程链中所给定的索引处。

.isLastEnabledPass ( passIndex : Integer ) : Boolean
passIndex – 被用于检查的过程

如果给定索引的过程在过程链中是最后一个启用的过程,则返回true。 由EffectComposer所使用,来决定哪一个过程应当被渲染到屏幕上。

.removePass ( pass : Pass ) : undefined
pass – 要从传递链中删除的传递。

从传递链中删除给定的传递。

.render ( deltaTime : Float ) : undefined
deltaTime – 增量时间值。

执行所有启用的后期处理过程,来产生最终的帧,

.reset ( renderTarget : WebGLRenderTarget ) : undefined
renderTarget – (可选)一个预先配置的渲染目标,内部由 EffectComposer 使用。

重置所有EffectComposer的内部状态。

.setPixelRatio ( pixelRatio : Float ) : undefined
pixelRatio – 设备像素比

设置设备的像素比。该值通常被用于HiDPI设备,以阻止模糊的输出。 因此,该方法语义类似于WebGLRenderer.setPixelRatio()。

.setSize ( width : Integer, height : Integer ) : undefined
width – EffectComposer的宽度。
height – EffectComposer的高度。

考虑设备像素比,重新设置内部渲染缓冲和过程的大小为(width, height)。 因此,该方法语义类似于WebGLRenderer.setSize()。

.swapBuffers () : undefined
交换内部的读/写缓冲。

1.2 ☘️THREE.RenderPass

THREE.RenderPass用于将场景渲染到中间缓冲区,为后续的后期处理效果(如模糊、色调调整等)提供基础。

1.2.1 ☘️构造函数

RenderPass(scene, camera, overrideMaterial, clearColor, clearAlpha)

  • scene THREE.Scene 要渲染的 Three.js 场景对象。
  • camera THREE.Camera 场景对应的相机(如 PerspectiveCamera)。
  • overrideMaterial THREE.Material (可选) 覆盖场景中所有物体的材质(默认 null)。
  • clearColor THREE.Color (可选) 渲染前清除画布的颜色(默认不主动清除)。
  • clearAlpha number (可选) 清除画布的透明度(默认 0)。

1.2.2 ☘️属性

.enabled:boolean
是否启用此通道(默认 true)。设为 false 可跳过渲染。

.clear:boolean
渲染前是否清除画布(默认 true)。若需叠加多个 RenderPass,可设为 false。

.needsSwap:boolean
是否需要在渲染后交换缓冲区(通常保持默认 false)。

1.2.3 ☘️方法

.setSize(width, height)
调整通道的渲染尺寸(通常由 EffectComposer 自动调用)。
width: 画布宽度(像素)。
height: 画布高度(像素)。

1.3 ☘️THREE.UnrealBloomPass

THREE.UnrealBloomPass是 是 Three.js 中用于实现 虚幻引擎风格泛光效果(Bloom) 的后期处理通道。它通过模拟光线散射和光晕效果,增强场景中高光区域的视觉表现。

1.3.1 ☘️构造函数

THREE.UnrealBloomPass(
new THREE.Vector2(width, height), // 渲染目标尺寸(通常与画布一致)
strength, // 泛光强度 (默认 1)
radius, // 泛光半径 (默认 0)
threshold // 泛光阈值 (默认 0)
)

new THREE.Vector2(width, height)
渲染目标的分辨率,通常与画布尺寸一致(如 new THREE.Vector2(window.innerWidth, window.innerHeight))。
strength(强度)
控制泛光效果的强度(亮度)。值越大,泛光越明显。
范围:0(无效果)到 3(强烈)。
radius(半径)
控制泛光的扩散半径。值越大,光晕范围越广。
范围:0(无扩散)到 1(较大扩散)。
threshold(阈值)
仅对亮度高于此值的像素应用泛光。值越低,更多区域会被处理。
范围:0(所有像素)到 1(仅最亮像素)。

1.3.2 ☘️使用示例

// 初始化
const composer = new THREE.EffectComposer(renderer);
const renderPass = new THREE.RenderPass(scene, camera);
const bloomPass = new THREE.UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight),1.2, 0.2, 0.8
);// 添加通道
composer.addPass(renderPass);
composer.addPass(bloomPass);// 渲染
function animate() {requestAnimationFrame(animate);composer.render();
}

1.3.3 ☘️方法

.setSize(width, height)
调整通道的渲染尺寸(通常由 EffectComposer 自动调用)。
width: 画布宽度(像素)。
height: 画布高度(像素)。

.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive)
内部方法,通常由 EffectComposer 自动调用,无需手动执行。

二、🍀超炫银河黑洞效果模拟

1. ☘️实现思路

通过EffectComposer后期处理组合器,RenderPass、UnrealBloomPass(泛光)后期处理通道,以及自定义shader着色器实现银河黑洞效果。具体代码参考代码样例。可以直接运行。

2. ☘️代码样例

<!DOCTYPE html>
<html lang="en">
<style>body {margin: 0;overflow: hidden;background-color: #000003;color: #fff;font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;}canvas {display: block;width: 100%;height: 100%;}#info {position: absolute;top: 15px;width: 100%;text-align: center;color: rgba(255, 255, 255, 0.9);font-size: 18px;letter-spacing: 0.5px;pointer-events: none;z-index: 100;text-shadow: 0 1px 4px rgba(0, 0, 0, 0.7);transition: opacity 1.5s ease-in-out 1s;}.ui-panel {position: absolute;background-color: rgba(25, 30, 50, 0.5);backdrop-filter: blur(10px) saturate(180%);-webkit-backdrop-filter: blur(10px) saturate(180%);padding: 12px 15px;border-radius: 10px;border: 1px solid rgba(255, 255, 255, 0.1);color: rgba(220, 220, 255, 0.9);font-size: 14px;user-select: none;z-index: 50;opacity: 0.8;transition: opacity 0.3s ease, background-color 0.3s ease, box-shadow 0.3s ease, bottom 0.3s ease;box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);box-sizing: border-box;display: flex;flex-direction: column;gap: 8px;}.ui-panel:hover {opacity: 1;background-color: rgba(35, 40, 60, 0.6);box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);}#controls {bottom: 20px;right: 20px;align-items: flex-start;}.control-item {cursor: pointer;padding: 5px;display: flex;align-items: center;gap: 6px;width: 100%;transition: color 0.2s ease;}.control-item:hover {color: #fff;}#theme-changer {bottom: 20px;left: 20px;display: flex;flex-direction: column;gap: 8px;}#theme-changer h4 {margin: 0 0 5px 0;font-weight: 500;font-size: 13px;color: rgba(200, 200, 230, 0.8);text-align: center;border-bottom: 1px solid rgba(255, 255, 255, 0.1);padding-bottom: 5px;}#theme-buttons {display: flex;gap: 8px;flex-wrap: wrap;justify-content: center;}.theme-button {padding: 6px 12px;border: 1px solid rgba(255, 255, 255, 0.2);border-radius: 6px;background-color: rgba(255, 255, 255, 0.1);color: rgba(220, 220, 255, 0.85);cursor: pointer;font-size: 12px;transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.1s ease;text-align: center;}.theme-button:hover {background-color: rgba(255, 255, 255, 0.2);border-color: rgba(255, 255, 255, 0.4);}.theme-button.active {background-color: rgba(120, 120, 255, 0.3);border-color: rgba(150, 150, 255, 0.6);color: #fff;font-weight: 500;transform: scale(1.02);}.theme-button:active {transform: scale(0.98);}.ui-icon {width: 1em;height: 1em;stroke: currentColor;stroke-width: 2;fill: none;stroke-linecap: round;stroke-linejoin: round;}@media (max-width: 640px) {.ui-panel {padding: 10px 12px;}#controls {bottom: 105px;right: 15px;max-width: calc(50% - 25px);min-width: 140px;}#theme-changer {bottom: 15px;left: 15px;max-width: calc(50% - 25px);min-width: 130px;}#theme-buttons {justify-content: flex-start;}.theme-button {padding: 5px 10px;font-size: 11px;}#info {font-size: 16px;}#info span {font-size: 12px;}}@media (max-width: 380px) {#controls {bottom: auto;top: 15px;right: 15px;left: auto;max-width: calc(100% - 30px);}#theme-changer {bottom: 15px;left: 15px;right: 15px;max-width: none;width: calc(100% - 30px);}#theme-buttons {justify-content: center;}}
</style>
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<div id="info">Galactic Black Hole<br><span style="font-size: 14px; opacity: 0.8;">Click core or button for Disk Echo. Drag to rotate.</span>
</div><div id="controls" class="ui-panel"><div id="autoRotateToggle" class="control-item" title="Toggle automatic rotation"></div><div id="triggerEffectButton" class="control-item" title="Trigger Disk Echo"></div>
</div><div id="theme-changer" class="ui-panel"><h4>Color Theme</h4><div id="theme-buttons"><button class="theme-button active" data-theme="inferno">Inferno</button><button class="theme-button" data-theme="ruby">Ruby</button><button class="theme-button" data-theme="plasma">Plasma</button><button class="theme-button" data-theme="void">Void</button></div>
</div>
<script type="module">import * as THREE from "https://esm.sh/three";import {OrbitControls} from 'https://esm.sh/three/examples/jsm/controls/OrbitControls.js';import {EffectComposer} from 'https://esm.sh/three/examples/jsm/postprocessing/EffectComposer.js';import {RenderPass} from 'https://esm.sh/three/examples/jsm/postprocessing/RenderPass.js';import {UnrealBloomPass} from 'https://esm.sh/three/examples/jsm/postprocessing/UnrealBloomPass.js';const BLACK_HOLE_EVENT_HORIZON_RADIUS = 1.0;const DISK_INNER_RADIUS = BLACK_HOLE_EVENT_HORIZON_RADIUS + 0.15;const DISK_OUTER_RADIUS = 5.5;const LENSING_SPHERE_RADIUS = BLACK_HOLE_EVENT_HORIZON_RADIUS + 0.07;const GLOW_RADIUS_FACTOR = 1.07;const PHOTON_SPHERE_RADIUS = BLACK_HOLE_EVENT_HORIZON_RADIUS * 1.5;const scene = new THREE.Scene();scene.fog = new THREE.FogExp2(0x000004, 0.085);const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 2000);const renderer = new THREE.WebGLRenderer({antialias: true,powerPreference: "high-performance"});renderer.setSize(window.innerWidth, window.innerHeight);renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));renderer.outputColorSpace = THREE.SRGBColorSpace;renderer.toneMapping = THREE.ACESFilmicToneMapping;renderer.toneMappingExposure = 0.95;document.body.appendChild(renderer.domElement);const composer = new EffectComposer(renderer);const renderPass = new RenderPass(scene, camera);composer.addPass(renderPass);const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight),0.7,0.7,0.75);composer.addPass(bloomPass);const controls = new OrbitControls(camera, renderer.domElement);controls.enableDamping = true;controls.dampingFactor = 0.04;controls.rotateSpeed = 0.6;controls.autoRotate = false;controls.autoRotateSpeed = 0.12;controls.target.set(0, 0, 0);controls.minDistance = 2.5;controls.maxDistance = 100;controls.enablePan = false;let autoRotateEnabled = false;const autoRotateToggle = document.getElementById('autoRotateToggle');const rotateIconSVG = `<svg class="ui-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M23 4v6h-6"></path><path d="M1 20v-6h6"></path><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`;function updateAutoRotateText() {autoRotateToggle.innerHTML = rotateIconSVG + `<span>Auto-Rotate: ${autoRotateEnabled ? "ON" : "OFF"}</span>`;}updateAutoRotateText();autoRotateToggle.addEventListener('click', () => {autoRotateEnabled = !autoRotateEnabled;controls.autoRotate = autoRotateEnabled;updateAutoRotateText();});const triggerEffectButton = document.getElementById('triggerEffectButton');const effectIconSVG = `<svg class="ui-icon" viewBox="0 0 24 24" style="stroke-width:1.5;" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="2"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="8"/></svg>`;triggerEffectButton.innerHTML = effectIconSVG + `<span>Disk Echo</span>`;triggerEffectButton.addEventListener('click', () => {triggerDiskEcho();});const starGeometry = new THREE.BufferGeometry();const starCount = 45000;const starPositions = new Float32Array(starCount * 3);const starColors = new Float32Array(starCount * 3);const starSizes = new Float32Array(starCount);const starAlphas = new Float32Array(starCount);const starFieldRadius = 1200;const baseColor = new THREE.Color(0xffffff);const blueColor = new THREE.Color(0xaaddff);const yellowColor = new THREE.Color(0xffffaa);const redColor = new THREE.Color(0xffcccc);for (let i = 0; i < starCount; i++) {const i3 = i * 3;const goldenRatio = (1 + Math.sqrt(5)) / 2;const theta = 2 * Math.PI * i / goldenRatio;const phi = Math.acos(1 - 2 * (i + 0.5) / starCount);const radius = Math.cbrt(Math.random()) * starFieldRadius;starPositions[i3] = radius * Math.sin(phi) * Math.cos(theta);starPositions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta);starPositions[i3 + 2] = radius * Math.cos(phi);const starColor = baseColor.clone();const colorType = Math.random();let colorIntensity = Math.random() * 0.4 + 0.6;if (colorType < 0.5) {starColor.lerp(blueColor, Math.random() * 0.3);} else if (colorType < 0.85) {starColor.lerp(yellowColor, Math.random() * 0.2);colorIntensity *= 0.9;} else {starColor.lerp(redColor, Math.random() * 0.15);colorIntensity *= 0.8;}starColor.multiplyScalar(colorIntensity);starColors[i3] = starColor.r;starColors[i3 + 1] = starColor.g;starColors[i3 + 2] = starColor.b;const sizeVariation = Math.random();if (sizeVariation > 0.997) {starSizes[i] = THREE.MathUtils.randFloat(1.5, 2.2);} else if (sizeVariation > 0.98) {starSizes[i] = THREE.MathUtils.randFloat(0.8, 1.5);} else {starSizes[i] = THREE.MathUtils.randFloat(0.3, 0.8);}const distFactor = Math.min(1.0, radius / starFieldRadius);starSizes[i] *= (1.0 - distFactor * 0.3);starAlphas[i] = Math.random() * 0.5 + 0.5;}starGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));starGeometry.setAttribute('color', new THREE.BufferAttribute(starColors, 3));starGeometry.setAttribute('size', new THREE.BufferAttribute(starSizes, 1));starGeometry.setAttribute('alpha', new THREE.BufferAttribute(starAlphas, 1));const starMaterial = new THREE.ShaderMaterial({uniforms: {uTime: {value: 0.0},uDiskEchoActive: {value: 0.0},uDiskEchoIntensity: {value: 0.0}},vertexShader: `attribute float size;attribute float alpha;varying vec3 vColor;varying float vAlpha;uniform float uDiskEchoActive;uniform float uDiskEchoIntensity;void main() {vColor = color;vAlpha = alpha;vec3 adjustedPosition = position;if (uDiskEchoActive > 0.0) {float distFromCenter = length(position);float pushFactor = uDiskEchoIntensity * 0.025 * smoothstep(50.0, 300.0, distFromCenter);adjustedPosition = position * (1.0 + pushFactor);}vec4 mvPosition = modelViewMatrix * vec4(adjustedPosition, 1.0);gl_PointSize = size * (350.0 / -mvPosition.z) * (1.0 + uDiskEchoIntensity * 0.35);gl_Position = projectionMatrix * mvPosition;}`,fragmentShader: `uniform float uTime;uniform float uDiskEchoIntensity;varying vec3 vColor;varying float vAlpha;void main() {float r = length(gl_PointCoord - vec2(0.5, 0.5));float baseAlpha = 1.0 - smoothstep(0.45, 0.5, r);if (baseAlpha < 0.01) discard;float twinkleSpeed = vAlpha * 1.5 + 0.5 + uDiskEchoIntensity * 4.0;float twinkleRange = 0.15 + uDiskEchoIntensity * 0.4;float twinkle = sin(uTime * twinkleSpeed + vAlpha * 10.0) * twinkleRange + 0.9;vec3 finalColor = vColor * twinkle * (1.0 + uDiskEchoIntensity * 0.9);gl_FragColor = vec4(finalColor, baseAlpha * vAlpha * (1.0 + uDiskEchoIntensity * 0.45));}`,blending: THREE.AdditiveBlending,depthWrite: false,transparent: true,vertexColors: true});const stars = new THREE.Points(starGeometry, starMaterial);scene.add(stars);const blackHoleGeometry = new THREE.SphereGeometry(BLACK_HOLE_EVENT_HORIZON_RADIUS, 64, 32);const blackHoleMaterial = new THREE.MeshBasicMaterial({color: 0x000000});const blackHole = new THREE.Mesh(blackHoleGeometry, blackHoleMaterial);blackHole.renderOrder = 0;scene.add(blackHole);const themes = {inferno: {diskHot: new THREE.Color(0xffffff),diskMid: new THREE.Color(0xffaa33),diskEdge: new THREE.Color(0xcc331a),diskDeep: new THREE.Color(0x661a00),lensing: new THREE.Color(0xffcc66),glow: new THREE.Color(0xff8833),photonSphere: new THREE.Color(0xffbb44),primaryWave: new THREE.Color(0xffaa33),secondaryWave: new THREE.Color(0xff5500),tertiaryWave: new THREE.Color(0xffdd22)},ruby: {diskHot: new THREE.Color(0xFFE4E1),diskMid: new THREE.Color(0xE0115F),diskEdge: new THREE.Color(0x8B0000),diskDeep: new THREE.Color(0x550000),lensing: new THREE.Color(0xFF6347),glow: new THREE.Color(0xFF4500),photonSphere: new THREE.Color(0xFF7F50),primaryWave: new THREE.Color(0xFF4500),secondaryWave: new THREE.Color(0xE0115F),tertiaryWave: new THREE.Color(0xFF6347)},plasma: {diskHot: new THREE.Color(0xffffff),diskMid: new THREE.Color(0x66ff66),diskEdge: new THREE.Color(0x00cc4d),diskDeep: new THREE.Color(0x006626),lensing: new THREE.Color(0x99ff99),glow: new THREE.Color(0x66ff99),photonSphere: new THREE.Color(0x88ffaa),primaryWave: new THREE.Color(0x66ff99),secondaryWave: new THREE.Color(0x22ffaa),tertiaryWave: new THREE.Color(0xaaffcc)},void: {diskHot: new THREE.Color(0xffffff),diskMid: new THREE.Color(0x87cefa),diskEdge: new THREE.Color(0x1e90ff),diskDeep: new THREE.Color(0x00008b),lensing: new THREE.Color(0xb0e0e6),glow: new THREE.Color(0xadd8e6),photonSphere: new THREE.Color(0x99ccff),primaryWave: new THREE.Color(0xadd8e6),secondaryWave: new THREE.Color(0x1e90ff),tertiaryWave: new THREE.Color(0xb0e0e6)}};let currentThemeName = 'inferno';let currentTheme = themes[currentThemeName];const diskGeometry = new THREE.RingGeometry(DISK_INNER_RADIUS, DISK_OUTER_RADIUS, 128, 64);const diskMaterial = new THREE.ShaderMaterial({uniforms: {uTime: {value: 0},uColorHot: {value: new THREE.Color().copy(currentTheme.diskHot)},uColorMid: {value: new THREE.Color().copy(currentTheme.diskMid)},uColorEdge: {value: new THREE.Color().copy(currentTheme.diskEdge)},uColorDeep: {value: new THREE.Color().copy(currentTheme.diskDeep)},uCameraPosition: {value: camera.position},uRippleActive: {value: 0.0},uRippleStartTime: {value: 0.0},uRippleDuration: {value: 2.8},uPrimaryWaveColor: {value: new THREE.Color(currentTheme.primaryWave)},uSecondaryWaveColor: {value: new THREE.Color(currentTheme.secondaryWave)},uTertiaryWaveColor: {value: new THREE.Color(currentTheme.tertiaryWave)},uRippleMaxRadius: {value: DISK_OUTER_RADIUS},uRippleThickness: {value: DISK_OUTER_RADIUS * 0.12},uRippleIntensity: {value: 0.0},uRippleDistortionStrength: {value: 0.0}},vertexShader: `varying vec2 vUv;varying vec3 vPosition;varying float vRadius;uniform float uRippleDistortionStrength;uniform float uTime;void main() {vUv = uv;vPosition = position;vRadius = length(position.xy);vec3 adjustedPosition = position;if (uRippleDistortionStrength > 0.0) {float angle = atan(position.y, position.x);float distortionAmount = sin(angle * 10.0 + uTime * 7.0 + vRadius * 2.0) * 0.08 * uRippleDistortionStrength;adjustedPosition.z += distortionAmount;}gl_Position = projectionMatrix * modelViewMatrix * vec4(adjustedPosition, 1.0);}`,fragmentShader: `uniform float uTime;uniform vec3 uColorHot;uniform vec3 uColorMid;uniform vec3 uColorEdge;uniform vec3 uColorDeep;uniform vec3 uCameraPosition;varying vec2 vUv;varying vec3 vPosition;varying float vRadius;uniform float uRippleActive;uniform float uRippleStartTime;uniform float uRippleDuration;uniform vec3 uPrimaryWaveColor;uniform vec3 uSecondaryWaveColor;uniform vec3 uTertiaryWaveColor;uniform float uRippleMaxRadius;uniform float uRippleThickness;uniform float uRippleIntensity;float rand(vec2 n){return fract(sin(dot(n,vec2(12.9898,4.1414)))*43758.5453);}float noise(vec2 p){vec2 ip=floor(p);vec2 u=fract(p);u=u*u*(3.0-2.0*u);float res=mix(mix(rand(ip),rand(ip+vec2(1.0,0.0)),u.x),mix(rand(ip+vec2(0.0,1.0)),rand(ip+vec2(1.0,1.0)),u.x),u.y);return res*res;}float fbm(vec2 p, float timeOffset, float freq, int octaves) {float total=0.0;float amplitude=0.65;float persistence=0.5;for(int i=0;i<octaves;i++){float timeScale=0.6+0.12*float(i);float noiseVal = noise(p*freq+vec2(timeOffset*timeScale*0.45,timeOffset*timeScale*0.3));total+=amplitude*noiseVal;vec2 warpOffset=vec2(noiseVal*0.18,-noiseVal*0.12);p+=warpOffset*amplitude*0.5;freq*=2.0;amplitude*=persistence;}return total;}float vortexPattern(float dist, float angle, float time){float spiralStrength=5.8;float timeScale=0.6;float angleOffset=dist*0.28;float spiral=sin(angle*2.3+angleOffset+dist*spiralStrength-time*timeScale);return smoothstep(-0.38,0.68,spiral)*0.32;}float calculateRippleIntensity(float dist, float rippleProgress, float currentRippleRadius, float thickness, float speedFactor) {if (rippleProgress <= 0.0 || rippleProgress >= 1.0) return 0.0;float distToRippleCenter = abs(dist - currentRippleRadius);float halfThickness = thickness * 0.5 * mix(1.0, 0.25, rippleProgress);float waveEnergyFactor = pow(1.0 - rippleProgress, 0.8 * speedFactor);float waveShape = smoothstep(halfThickness, halfThickness - (thickness * 0.25), distToRippleCenter);float angle = atan(vPosition.y, vPosition.x);float angleMod = sin(angle * 10.0 + rippleProgress * 15.0) * 0.15 + 0.9;return waveShape * waveEnergyFactor * angleMod;}void main(){float dist = vRadius;float innerEdge = ${DISK_INNER_RADIUS.toFixed(2)};float outerEdge = ${DISK_OUTER_RADIUS.toFixed(2)};float normalizedPos = clamp((dist - innerEdge) / (outerEdge - innerEdge), 0.0, 1.0);float angle = atan(vPosition.y, vPosition.x);float orbitalVelocity = 1.0 / sqrt(max(dist, 0.1));float dopplerFactor = 0.0; float beamingFactor = 1.0;if (length(uCameraPosition) > 0.01) {vec3 tangentialDirection = normalize(vec3(-vPosition.y, vPosition.x, 0.0));vec3 toCamera = normalize(uCameraPosition - vPosition);dopplerFactor = dot(toCamera, tangentialDirection) * orbitalVelocity * 0.3;beamingFactor = 1.0 + dopplerFactor * 0.4;beamingFactor = clamp(beamingFactor, 0.5, 2.0);}float rotationSpeedFactor = 4.8/(pow(dist,1.6)+1.1);float rotatedAngle = angle-uTime*rotationSpeedFactor*0.52;vec2 baseCoord = vec2(dist*1.9, rotatedAngle*3.6);float evolvingTime = uTime*0.17;float noiseValueFast = fbm(baseCoord, evolvingTime * 1.2, 2.2, 6);float noiseValueSlow = fbm(baseCoord * 0.6, evolvingTime * 0.5, 1.5, 4);float noiseValue = noiseValueFast * 0.7 + noiseValueSlow * 0.4;float vortexValue = vortexPattern(dist, angle, uTime);float finalPattern = noiseValue*0.8 + vortexValue*1.1;float temperature = orbitalVelocity * (1.0 + finalPattern * 0.3);temperature = clamp(temperature, 0.0, 2.0);vec3 colorInner = mix(uColorHot, uColorMid, smoothstep(0.0, 0.40, normalizedPos) * (1.0 - temperature * 0.3));vec3 colorOuterBlend = mix(uColorMid, uColorEdge, smoothstep(0.40, 0.80, normalizedPos));vec3 colorDeepBlend = mix(uColorEdge, uColorDeep, smoothstep(0.80, 1.0, normalizedPos));vec3 color = mix(colorInner, colorOuterBlend, smoothstep(0.40, 0.80, normalizedPos));color = mix(color, colorDeepBlend, smoothstep(0.80, 1.0, normalizedPos));float redshiftFactor = dopplerFactor * 0.15;vec3 redshift = vec3(1.0 + redshiftFactor, 1.0, 1.0 - redshiftFactor);color *= redshift;float patternBrightness = (finalPattern+0.5)*1.15;patternBrightness += pow(max(0.0,finalPattern-0.5),1.3)*0.6;float radialBrightness = pow(1.0-smoothstep(0.0,0.8,normalizedPos),1.9)*3.0+0.25;float finalBrightness = patternBrightness*radialBrightness*beamingFactor;float combinedRippleIntensity = 0.0;vec3 rippleColorContribution = vec3(0.0);if (uRippleActive > 0.5) {float rippleTime = uTime - uRippleStartTime;float rippleProgress = clamp(rippleTime / uRippleDuration, 0.0, 1.0);float primarySpeed = 1.0;float primaryRadius = mix(innerEdge, uRippleMaxRadius, rippleProgress * primarySpeed);float primaryIntensity = calculateRippleIntensity(dist, rippleProgress, primaryRadius, uRippleThickness, primarySpeed);float secondarySpeed = 0.75;float secondaryProgress = max(0.0, rippleProgress - 0.1) * secondarySpeed;float secondaryRadius = mix(innerEdge, uRippleMaxRadius * 0.85, secondaryProgress);float secondaryIntensity = calculateRippleIntensity(dist, secondaryProgress, secondaryRadius, uRippleThickness * 0.8, secondarySpeed) * 0.8;float tertiarySpeed = 0.5;float tertiaryProgress = max(0.0, rippleProgress - 0.2) * tertiarySpeed;float tertiaryRadius = mix(innerEdge, uRippleMaxRadius * 0.7, tertiaryProgress);float tertiaryIntensity = calculateRippleIntensity(dist, tertiaryProgress, tertiaryRadius, uRippleThickness * 0.6, tertiarySpeed) * 0.6;combinedRippleIntensity = primaryIntensity + secondaryIntensity + tertiaryIntensity;rippleColorContribution = uPrimaryWaveColor * primaryIntensity +uSecondaryWaveColor * secondaryIntensity +uTertiaryWaveColor * tertiaryIntensity;float sparkleNoiseVal = rand(vUv * vec2(300.0, 500.0) + uTime * vec2(20.0 + primaryIntensity * 10.0, 30.0 + primaryIntensity * 15.0) );float sparkleThreshold = 0.985 - primaryIntensity * 0.03;if (primaryIntensity > 0.02 && sparkleNoiseVal > sparkleThreshold) {float sparkleBrightness = pow((sparkleNoiseVal - sparkleThreshold) / (1.0 - sparkleThreshold), 2.0);rippleColorContribution += mix(uPrimaryWaveColor, vec3(1.0), 0.6) * primaryIntensity * sparkleBrightness * 10.0 * uRippleIntensity;}float afterglowPulse = sin(rippleProgress * 15.0) * 0.5 + 0.5;float afterglowIntensity = smoothstep(0.0, 0.3, rippleProgress) * (1.0 - rippleProgress) * 0.4 * afterglowPulse;combinedRippleIntensity += afterglowIntensity * smoothstep(innerEdge, innerEdge + 1.5, dist);}float rippleBoost = combinedRippleIntensity * 9.0 * uRippleIntensity;color *= (finalBrightness + rippleBoost);if (combinedRippleIntensity * uRippleIntensity > 0.01) {float shimmerEffect = sin(angle * 20.0 + uTime * 10.0 + dist * 5.0) * 0.15 + 0.9;vec3 currentRippleColors = rippleColorContribution * shimmerEffect;color = mix(color, currentRippleColors * 1.8, min(1.0, combinedRippleIntensity * uRippleIntensity * 1.5));}float hotBoost = smoothstep(3.0, 5.0, finalBrightness + rippleBoost) * smoothstep(0.0, 0.1, normalizedPos);color = mix(color, vec3(1.0, 1.0, 1.0), hotBoost * 0.45);float innerAlpha = smoothstep(0.0, 0.06, normalizedPos);float outerAlpha = 1.0 - smoothstep(0.85, 1.0, normalizedPos);float noiseAlphaFactor = clamp(finalPattern * 0.35 + 0.75, 0.65, 1.0);float alpha = innerAlpha * outerAlpha * noiseAlphaFactor;float rippleAlphaBoost = combinedRippleIntensity * 0.9 * uRippleIntensity;color = clamp(color, 0.0, 8.0);gl_FragColor = vec4(color, clamp(alpha + rippleAlphaBoost, 0.0, 1.0));}`,transparent: true,side: THREE.DoubleSide,depthWrite: false,blending: THREE.AdditiveBlending});const accretionDisk = new THREE.Mesh(diskGeometry, diskMaterial);accretionDisk.rotation.x = Math.PI / 2.6;accretionDisk.renderOrder = 1;scene.add(accretionDisk);const photonSphereGeometry = new THREE.SphereGeometry(PHOTON_SPHERE_RADIUS, 64, 32);const photonSphereMaterial = new THREE.ShaderMaterial({uniforms: {uTime: {value: 0},uColor: {value: new THREE.Color().copy(currentTheme.photonSphere)},uDiskEchoActive: {value: 0.0},uDiskEchoIntensity: {value: 0.0}},vertexShader: `varying vec3 vNormal;varying vec3 vViewPosition;void main() {vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);vViewPosition = -mvPosition.xyz;vNormal = normalize(normalMatrix * normal);gl_Position = projectionMatrix * mvPosition;}`,fragmentShader: `uniform float uTime;uniform vec3 uColor;uniform float uDiskEchoActive;uniform float uDiskEchoIntensity;varying vec3 vNormal;varying vec3 vViewPosition;void main() {vec3 viewDir = normalize(vViewPosition);float fresnel = pow(1.0 - abs(dot(viewDir, vNormal)), 3.0);float pulseRate = 2.0 + uDiskEchoIntensity * 8.0;float pulseDepth = 0.1 + uDiskEchoIntensity * 0.5;float pulse = sin(uTime * pulseRate) * pulseDepth + 0.9;float alpha = fresnel * (0.3 + uDiskEchoIntensity * 0.6) * pulse;vec3 finalColor = uColor;if (uDiskEchoActive > 0.5) {float colorPulse = sin(uTime * 4.0 + dot(vNormal, vec3(1.0)) * 5.0) * 0.5 + 0.5;finalColor = mix(finalColor, finalColor * vec3(1.4, 1.2, 0.8), colorPulse * uDiskEchoIntensity * 1.2);finalColor *= (1.0 + uDiskEchoIntensity * 0.7);}gl_FragColor = vec4(finalColor, alpha);}`,transparent: true,side: THREE.FrontSide,depthWrite: false,blending: THREE.AdditiveBlending});const photonSphere = new THREE.Mesh(photonSphereGeometry, photonSphereMaterial);photonSphere.renderOrder = 4;scene.add(photonSphere);const lensingGeometry = new THREE.SphereGeometry(LENSING_SPHERE_RADIUS, 64, 32);const lensingMaterial = new THREE.ShaderMaterial({uniforms: {uTime: {value: 0},uLensingColor: {value: new THREE.Color().copy(currentTheme.lensing)},uDiskEchoActive: {value: 0.0},uDiskEchoIntensity: {value: 0.0}},vertexShader: `varying vec3 vNormal;varying vec3 vViewPosition;void main() {vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);vViewPosition = -mvPosition.xyz;vNormal = normalize(normalMatrix * normal);gl_Position = projectionMatrix * mvPosition;}`,fragmentShader: `uniform float uTime;uniform vec3 uLensingColor;uniform float uDiskEchoActive;uniform float uDiskEchoIntensity;varying vec3 vNormal;varying vec3 vViewPosition;float fresnel(vec3 d,vec3 n,float p){return pow(1.0-abs(dot(normalize(d),n)),p);}float rand(vec2 n){return fract(sin(dot(n,vec2(12.9898,4.1414)))*43758.5453);}float noise(vec2 p){vec2 ip=floor(p);vec2 u=fract(p);u=u*u*(3.0-2.0*u);float res=mix(mix(rand(ip),rand(ip+vec2(1.0,0.0)),u.x),mix(rand(ip+vec2(0.0,1.0)),rand(ip+vec2(1.0,1.0)),u.x),u.y);return res*res;}void main(){vec3 viewDir=normalize(vViewPosition);float fresnelPower = 5.2 - uDiskEchoIntensity * 1.5;float fF=fresnel(viewDir,vNormal,fresnelPower);float pulseSpeed = 0.55 + uDiskEchoIntensity * 3.0;float pulseDepth = 0.12 + uDiskEchoIntensity * 0.4;float p=(sin(uTime*pulseSpeed+length(vViewPosition)*0.12)*pulseDepth+0.95);float noiseScale = 7.0 + uDiskEchoIntensity * 5.0;float noiseSpeed = 0.35 + uDiskEchoIntensity * 1.2;vec2 nC=vNormal.xy*noiseScale+uTime*noiseSpeed;float nV=noise(nC)*(0.12 + uDiskEchoIntensity * 0.15);vec3 dN=normalize(vNormal+vec3(nV,nV*0.6,0.0));float alphaBase = 0.68 + uDiskEchoIntensity * 0.5;float a=fF*alphaBase*p;float edgePower = 8.5 - uDiskEchoIntensity * 3.5;a+=pow(1.0-abs(dot(viewDir,dN)),edgePower)*(0.38 + uDiskEchoIntensity * 0.6);vec3 finalColor = uLensingColor;if (uDiskEchoActive > 0.5) {float colorShift = dot(viewDir, vNormal) * 0.5 + 0.5;finalColor = mix(finalColor, finalColor * vec3(1.3, 1.1, 0.9), colorShift * uDiskEchoIntensity);finalColor *= (1.0 + uDiskEchoIntensity * 0.4);}gl_FragColor=vec4(finalColor, clamp(a,0.0,1.0)*0.90);}`,transparent: true,side: THREE.FrontSide,depthWrite: false,blending: THREE.AdditiveBlending});const lensingEffectSphere = new THREE.Mesh(lensingGeometry, lensingMaterial);lensingEffectSphere.scale.multiplyScalar(1.62);lensingEffectSphere.renderOrder = 2;scene.add(lensingEffectSphere);const glowGeometry = new THREE.SphereGeometry(BLACK_HOLE_EVENT_HORIZON_RADIUS, 64, 32);const glowMaterial = new THREE.ShaderMaterial({uniforms: {uTime: {value: 0},uGlowColor: {value: new THREE.Color().copy(currentTheme.glow)},uDiskEchoActive: {value: 0.0},uDiskEchoIntensity: {value: 0.0},uDiskEchoColor: {value: new THREE.Color().copy(currentTheme.primaryWave)}},vertexShader: `varying vec3 vNormal;varying vec3 vViewPosition;void main() {vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);vViewPosition = -mvPosition.xyz;vNormal = normalize(normalMatrix * normal);gl_Position = projectionMatrix * mvPosition;}`,fragmentShader: `uniform float uTime;uniform vec3 uGlowColor;uniform float uDiskEchoActive;uniform float uDiskEchoIntensity;uniform vec3 uDiskEchoColor;varying vec3 vNormal;varying vec3 vViewPosition;float rand(vec2 n){return fract(sin(dot(n,vec2(12.9898,4.1414)))*43758.5453);}float noise(vec2 p){vec2 ip=floor(p);vec2 u=fract(p);u=u*u*(3.0-2.0*u);float res=mix(mix(rand(ip),rand(ip+vec2(1.0,0.0)),u.x),mix(rand(ip+vec2(0.0,1.0)),rand(ip+vec2(1.0,1.0)),u.x),u.y);return res*res;}void main(){float glowPower = 2.6 - uDiskEchoIntensity * 1.2;float i=pow(0.68-dot(vNormal,normalize(vViewPosition)), glowPower);float pulseSpeed = 0.7 + uDiskEchoIntensity * 7.0;float pulseDepth = 0.18 + uDiskEchoIntensity * 0.5;float p=sin(uTime*pulseSpeed+vNormal.y*1.8)*pulseDepth+0.88;float noiseScale = 9.0 + uDiskEchoIntensity * 8.0;float noiseSpeed = 1.8 + uDiskEchoIntensity * 6.0;float f=noise(vNormal.xz*noiseScale+uTime*noiseSpeed)*(0.35 + uDiskEchoIntensity * 0.25)+0.75;float fI=clamp(i*p*f,0.0,1.0)*(0.92 + uDiskEchoIntensity * 0.5);vec3 finalColor = uGlowColor;if (uDiskEchoActive > 0.5) {float flarePattern = noise(vNormal.xy * 15.0 + uTime * 3.0) * noise(vNormal.yz * 12.0 + uTime * 2.0);float flarePulse = sin(uTime * 8.0 + flarePattern * 10.0) * 0.5 + 0.5;vec3 flareColor = mix(uGlowColor, uDiskEchoColor, flarePulse);finalColor = mix(uGlowColor, flareColor * 1.8, uDiskEchoIntensity * flarePulse * 1.2);finalColor *= (1.0 + uDiskEchoIntensity * 0.8);}gl_FragColor=vec4(finalColor, fI);}`,transparent: true,side: THREE.BackSide,blending: THREE.AdditiveBlending,depthWrite: false});const glowEffect = new THREE.Mesh(glowGeometry, glowMaterial);glowEffect.scale.multiplyScalar(GLOW_RADIUS_FACTOR * 1.16);glowEffect.renderOrder = 3;scene.add(glowEffect);let lastRippleTime = -Infinity;const RIPPLE_COOLDOWN = 0.5;let diskEchoIntensity = 0.0;let diskEchoActive = false;let diskEchoStartTime = 0;const DISK_ECHO_DURATION = 2.8;function triggerDiskEcho() {const currentTime = clock.getElapsedTime();if (currentTime - lastRippleTime < RIPPLE_COOLDOWN) {return;}lastRippleTime = currentTime;diskEchoStartTime = currentTime;diskEchoActive = true;diskMaterial.uniforms.uRippleActive.value = 1.0;diskMaterial.uniforms.uRippleStartTime.value = currentTime;diskMaterial.uniforms.uPrimaryWaveColor.value.copy(themes[currentThemeName].primaryWave).multiplyScalar(3.0);diskMaterial.uniforms.uSecondaryWaveColor.value.copy(themes[currentThemeName].secondaryWave).multiplyScalar(2.7);diskMaterial.uniforms.uTertiaryWaveColor.value.copy(themes[currentThemeName].tertiaryWave).multiplyScalar(2.4);glowMaterial.uniforms.uDiskEchoColor.value.copy(themes[currentThemeName].primaryWave).multiplyScalar(1.8);bloomPass.strength = 1.3;bloomPass.threshold = 0.60;}const raycaster = new THREE.Raycaster();const pointer = new THREE.Vector2();function onPointerDown(event) {if (event.target.closest('.ui-panel')) return;if (event.isPrimary === false && event.pointerType !== 'touch') return;let x, y;if (event.touches && event.touches.length > 0) {x = event.touches[0].clientX;y = event.touches[0].clientY;} else {x = event.clientX;y = event.clientY;}pointer.x = (x / window.innerWidth) * 2 - 1;pointer.y = -(y / window.innerHeight) * 2 + 1;raycaster.setFromCamera(pointer, camera);const intersects = raycaster.intersectObject(blackHole, false);if (intersects.length > 0) triggerDiskEcho();}renderer.domElement.addEventListener('pointerdown', onPointerDown, false);const themeButtonsContainer = document.getElementById('theme-buttons');themeButtonsContainer.addEventListener('click', (event) => {const button = event.target.closest('.theme-button');if (button) {const themeName = button.dataset.theme;if (themes[themeName] && themeName !== currentThemeName) {currentThemeName = themeName;currentTheme = themes[currentThemeName];diskMaterial.uniforms.uColorHot.value.copy(currentTheme.diskHot);diskMaterial.uniforms.uColorMid.value.copy(currentTheme.diskMid);diskMaterial.uniforms.uColorEdge.value.copy(currentTheme.diskEdge);diskMaterial.uniforms.uColorDeep.value.copy(currentTheme.diskDeep);lensingMaterial.uniforms.uLensingColor.value.copy(currentTheme.lensing);glowMaterial.uniforms.uGlowColor.value.copy(currentTheme.glow);photonSphereMaterial.uniforms.uColor.value.copy(currentTheme.photonSphere);diskMaterial.uniforms.uPrimaryWaveColor.value.copy(currentTheme.primaryWave).multiplyScalar(3.0);diskMaterial.uniforms.uSecondaryWaveColor.value.copy(currentTheme.secondaryWave).multiplyScalar(2.7);diskMaterial.uniforms.uTertiaryWaveColor.value.copy(currentTheme.tertiaryWave).multiplyScalar(2.4);glowMaterial.uniforms.uDiskEchoColor.value.copy(currentTheme.primaryWave).multiplyScalar(1.8);themeButtonsContainer.querySelectorAll('.theme-button').forEach(btn => btn.classList.remove('active'));button.classList.add('active');}}});setTimeout(() => {const info = document.getElementById('info');if (info) info.style.opacity = '0';}, 7000);let resizeTimeout;window.addEventListener('resize', () => {clearTimeout(resizeTimeout);resizeTimeout = setTimeout(() => {camera.aspect = window.innerWidth / window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth, window.innerHeight);composer.setSize(window.innerWidth, window.innerHeight);bloomPass.resolution.set(window.innerWidth, window.innerHeight);renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));}, 150);});const clock = new THREE.Clock();function animate() {requestAnimationFrame(animate);const elapsedTime = clock.getElapsedTime();const deltaTime = clock.getDelta();diskMaterial.uniforms.uTime.value = elapsedTime;lensingMaterial.uniforms.uTime.value = elapsedTime;glowMaterial.uniforms.uTime.value = elapsedTime;starMaterial.uniforms.uTime.value = elapsedTime;photonSphereMaterial.uniforms.uTime.value = elapsedTime;diskMaterial.uniforms.uCameraPosition.value.copy(camera.position);if (diskEchoActive) {const timeSinceEchoStart = elapsedTime - diskEchoStartTime;const normalizedTime = timeSinceEchoStart / DISK_ECHO_DURATION;let intensityVal;if (normalizedTime < 0.07) {intensityVal = normalizedTime / 0.07;} else {const t = (normalizedTime - 0.07) / (1.0 - 0.07);intensityVal = Math.pow(1.0 - t, 1.8);intensityVal += Math.sin(t * Math.PI) * 0.35 * Math.pow(1.0 - t, 0.5);}diskEchoIntensity = Math.max(0.0, Math.min(1.0, intensityVal));const pulseFactor = Math.sin(elapsedTime * 15.0) * 0.15 + 1.0;diskEchoIntensity *= pulseFactor;diskEchoIntensity = Math.min(1.2, diskEchoIntensity);let distortionStrengthFactor = 0.0;if (normalizedTime < 0.4) {distortionStrengthFactor = Math.sin((normalizedTime / 0.4) * Math.PI);}diskMaterial.uniforms.uRippleDistortionStrength.value = distortionStrengthFactor * diskEchoIntensity * 2.0;if (timeSinceEchoStart >= DISK_ECHO_DURATION) {diskEchoActive = false;diskEchoIntensity = 0.0;diskMaterial.uniforms.uRippleActive.value = 0.0;diskMaterial.uniforms.uRippleDistortionStrength.value = 0.0;bloomPass.strength = 0.7;bloomPass.threshold = 0.75;}diskMaterial.uniforms.uRippleIntensity.value = diskEchoIntensity;starMaterial.uniforms.uDiskEchoActive.value = diskEchoActive ? 1.0 : 0.0;starMaterial.uniforms.uDiskEchoIntensity.value = diskEchoIntensity;photonSphereMaterial.uniforms.uDiskEchoActive.value = diskEchoActive ? 1.0 : 0.0;photonSphereMaterial.uniforms.uDiskEchoIntensity.value = diskEchoIntensity;lensingMaterial.uniforms.uDiskEchoActive.value = diskEchoActive ? 1.0 : 0.0;lensingMaterial.uniforms.uDiskEchoIntensity.value = diskEchoIntensity;glowMaterial.uniforms.uDiskEchoActive.value = diskEchoActive ? 1.0 : 0.0;glowMaterial.uniforms.uDiskEchoIntensity.value = diskEchoIntensity;}controls.update();stars.rotation.y += deltaTime * 0.004;stars.rotation.x += deltaTime * 0.0015;composer.render(deltaTime);}function initialCameraAnimation() {const startPosition = new THREE.Vector3(0, 15, 18);const endPosition = new THREE.Vector3(0, 5, 8);const duration = 4500;const startTime = Date.now();camera.position.copy(startPosition);controls.enabled = false;function updateCamera() {const elapsed = Date.now() - startTime;if (elapsed < duration) {const progress = elapsed / duration;const t = 1 - Math.pow(1 - progress, 5);camera.position.lerpVectors(startPosition, endPosition, t);controls.target.set(0, 0, 0);requestAnimationFrame(updateCamera);} else {camera.position.copy(endPosition);controls.target.set(0, 0, 0);controls.enabled = true;}}updateCamera();}window.onload = () => {initialCameraAnimation();animate();}
</script>
</body>
</html>

效果如下
在这里插入图片描述
参考:Three.js 银河黑洞模拟

相关文章:

  • 初识Docker:容器化技术的入门指南
  • 180 度 = π 弧度
  • [网页五子棋][匹配模块]前后端交互接口(消息推送机制)、客户端开发(匹配页面、匹配功能)
  • Android学习之定时任务
  • 大数据-273 Spark MLib - 基础介绍 机器学习算法 决策树 分类原则 分类原理 基尼系数 熵
  • 深入解析Linux死锁:原理、原因及解决方案
  • 《ChatGPT o3抗命:AI失控警钟还是成长阵痛?》
  • 基于对比学习的推荐系统开发方案,使用Python在PyCharm中实现
  • Transformer架构详解:从Attention到ChatGPT
  • Senna代码解读
  • spring sentinel
  • Linux `vi/vim` 编辑器深度解析与高阶应用指南
  • (25年5.28)ChatGPT Plus充值教程与实用指南:附国内外使用案例与模型排行
  • Service Worker介绍及应用(实现Web Push机制)
  • 华为AP6050DN无线接入点瘦模式转胖模式
  • 【数据结构初阶】顺序表的应用
  • PostgreSQL 内置扩展列表
  • 嵌入式通用集成电路卡市场潜力报告:物联网浪潮下的机遇与挑战剖析
  • Parasoft C++Test软件单元测试_实例讲解(对多次调用的函数打桩)
  • Java复习Day21
  • 网站开发和网页设计的区别/百度网站大全旧版
  • 网站设计规划的一般流程/2022近期重大新闻事件10条
  • 天津市工商网站查询企业信息/营销策划方案范文
  • 网站建设哪个比较好/全国新闻媒体发稿平台
  • 广州10打网站服务商/看b站二十四小时直播间
  • 关于政府网站建设的研究报告/东莞全网营销推广