👨⚕️ 主页: gis分享者
👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!
👨⚕️ 收录于专栏:threejs gis工程师
文章目录
一、🍀前言
本文详细介绍如何基于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对象中将该纹理作为输入参数
UnrealBloomPass 用于实现 虚幻引擎风格泛光效果(Bloom) 的后期处理通道。它通过模拟光线散射和光晕效果,增强场景中高光区域的视觉表现
OutputPass 主要用于对最终渲染结果进行色调映射(Tone Mapping)、Gamma 校正等输出调整。它通常作为 EffectComposer 的最后一个环节,将处理后的图像输出到屏幕
.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.4 ☘️THREE.FilmPass
THREE.FilmPass是 Three.js 后期处理模块中的一个特效通道,用于模拟电影胶片效果(如扫描线、颗粒噪声和画面抖动)。适用于复古风格或科幻场景的视觉增强。
1.4.1 ☘️构造函数
FilmPass(
noiseIntensity, // 噪声强度
scanlinesIntensity,// 扫描线强度
scanlinesCount, // 扫描线数量
grayscale // 是否转为灰度
)
1.4.2 ☘️属性
.enabled:boolean
是否启用此通道(默认 true)。设为 false 可临时禁用效果。
.uniforms:object
着色器 uniforms 对象,可直接修改参数(动态调整效果):
filmPass.uniforms.nIntensity.value = 0.8; // 调整噪声强度
filmPass.uniforms.sIntensity.value = 0.5; // 调整扫描线强度
filmPass.uniforms.sCount.value = 1024; // 调整扫描线密度
filmPass.uniforms.grayscale.value = 1; // 启用灰度(1 是,0 否)
1.4.3 ☘️方法
.setSize(width, height)
调整通道的渲染尺寸(通常由 EffectComposer 自动调用)。
width: 画布宽度(像素)。
height: 画布高度(像素)。
1.5 ☘️OutputPass
OutputPass 是 Three.js 后期处理(Post-Processing)流程中的一个通道(Pass),主要用于对最终渲染结果进行色调映射(Tone Mapping)、Gamma 校正等输出调整。它通常作为 EffectComposer 的最后一个环节,将处理后的图像输出到屏幕。
1.5.1 ☘️构造函数
OutputPass(renderer, scene, camera)
renderer:WebGLRenderer类型 Three.js 渲染器实例
scene:Scene类型 Three.js 场景对象
camera:Camera类型 Three.js 相机对象
1.4.2 ☘️使用示例
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js';
// 1. 创建 EffectComposer
const composer = new EffectComposer(renderer);
// 2. 添加渲染通道(RenderPass)
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
// 3. 添加 OutputPass(通常作为最后一个通道)
const outputPass = new OutputPass(renderer, scene, camera);
composer.addPass(outputPass);
// 4. 执行渲染循环
function animate() {
requestAnimationFrame(animate);
composer.render(); // 替代 renderer.render(scene, camera)
}
animate();
1.4.3 ☘️方法
.setSize(width, height)
调整通道的渲染尺寸(通常由 EffectComposer 自动调用)。
width: 画布宽度(像素)。
height: 画布高度(像素)。
.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive)
执行渲染流程,将处理后的图像输出到屏幕。
二、🍀交互式神经网络可视化
单击/点击通过自定义 GLSL 着色器发送动画能量脉冲,并在节点/连接扩展时使其变亮。包括主题和密度控制。
1. ☘️实现思路
通过EffectComposer后期处理组合器,RenderPass、UnrealBloomPass(泛光)、FilmPass(电影胶片效果)、OutputPass后期处理通道,以及自定义shader着色器实现交互式神经网络可视化。具体代码参考代码样例。可以直接运行。
2. ☘️代码样例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
</head>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
canvas {
display: block;
width: 100%;
height: 100%;
cursor: pointer;
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
.ui-panel {
position: absolute;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
background: rgba(0, 0, 0, .7);
border-radius: 12px;
border: 1px solid rgba(255, 120, 50, .3);
box-shadow: 0 4px 20px rgba(0, 0, 0, .5);
z-index: 10;
padding: 15px;
color: #eee;
font-family: 'Inter', sans-serif;
}
#instructions-container {
top: 20px;
left: 20px;
font-size: 14px;
line-height: 1.5;
max-width: 280px;
}
#instruction-title {
font-weight: 600;
margin-bottom: 6px;
font-size: 15px;
}
#theme-selector {
top: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 12px;
max-width: 150px;
}
#theme-selector-title {
font-weight: 600;
font-size: 15px;
margin-bottom: 2px;
}
.theme-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.theme-button {
width: 36px;
height: 36px;
border-radius: 8px;
border: 2px solid rgba(255, 255, 255, .3);
cursor: pointer;
transition: transform .2s, border-color .2s;
outline: none;
overflow: hidden;
}
.theme-button:hover, .theme-button:focus {
transform: scale(1.05);
border-color: rgba(255, 255, 255, .7);
}
.theme-button.active {
transform: scale(1.05);
border-color: rgba(255, 255, 255, .9);
box-shadow: 0 0 10px rgba(255, 200, 150, .6);
}
#theme-1 { background: linear-gradient(45deg, #4F46E5, #7C3AED, #C026D3, #DB2777); }
#theme-2 { background: linear-gradient(45deg, #F59E0B, #F97316, #DC2626, #7F1D1D); }
#theme-3 { background: linear-gradient(45deg, #EC4899, #8B5CF6, #6366F1, #3B82F6); }
#theme-4 { background: linear-gradient(45deg, #10B981, #A3E635, #FACC15, #FB923C); }
#density-controls {
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 8px;
}
.density-label {
font-size: 13px;
display: flex;
justify-content: space-between;
}
.density-slider {
width: 100%;
appearance: none;
height: 4px;
border-radius: 2px;
background: rgba(255, 120, 50, .3);
outline: none;
cursor: pointer;
}
.density-slider::-webkit-slider-thumb {
appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: rgba(255, 120, 50, .8);
cursor: pointer;
transition: transform .1s, background .1s;
}
.density-slider::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 50%;
background: rgba(255, 120, 50, .8);
cursor: pointer;
border: none;
transition: transform .1s, background .1s;
}
.density-slider::-webkit-slider-thumb:hover { transform: scale(1.1); background: rgba(255, 140, 50, 1); }
.density-slider::-moz-range-thumb:hover { transform: scale(1.1); background: rgba(255, 140, 50, 1); }
#control-buttons {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 15px;
z-index: 10;
background: rgba(0, 0, 0, .6);
padding: 10px 15px;
border-radius: 10px;
border: 1px solid rgba(255, 120, 50, .2);
}
.control-button {
background: rgba(255, 120, 50, .2);
color: #eee;
border: 1px solid rgba(255, 150, 50, .3);
padding: 8px 15px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: background-color 0.2s, transform 0.1s;
white-space: nowrap;
min-width: 80px;
text-align: center;
font-family: 'Inter', sans-serif;
}
.control-button:hover, .control-button:focus {
background: rgba(255, 120, 50, .4);
outline: none;
}
.control-button:active {
background: rgba(255, 120, 50, .6);
transform: scale(0.95);
}
@media (max-width: 640px) {
#instructions-container {
max-width: calc(100% - 40px);
font-size: 13px;
padding: 10px 15px;
top: 10px;
left: 10px;
}
#instruction-title {
font-size: 14px;
}
#theme-selector {
top: auto;
bottom: 20px;
right: 10px;
left: auto;
transform: none;
max-width: 120px;
padding: 10px;
}
#theme-selector-title {
font-size: 14px;
}
.theme-button {
width: 30px;
height: 30px;
}
.density-label { font-size: 12px; }
#control-buttons {
bottom: 10px;
gap: 10px;
padding: 8px 10px;
}
.control-button {
padding: 6px 10px;
font-size: 12px;
min-width: 65px;
}
}
@media (max-width: 400px) {
#theme-selector {
flex-direction: column;
align-items: center;
max-width: none;
width: calc(100% - 20px);
left: 10px;
right: 10px;
bottom: 75px;
}
.theme-grid {
grid-template-columns: repeat(4, 1fr);
width: 100%;
justify-items: center;
}
#density-controls {
width: 80%;
margin-top: 15px;
}
#control-buttons {
width: calc(100% - 20px);
justify-content: space-around;
}
}
</style>
<body>
<div id="instructions-container" class="ui-panel">
<div id="instruction-title">Interactive Neural Network</div>
<div>Click or tap to create energy pulses through the network. Drag to rotate.</div>
</div>
<div id="theme-selector" class="ui-panel">
<div id="theme-selector-title">Visual Theme</div>
<div class="theme-grid">
<button class="theme-button" id="theme-1" data-theme="0" aria-label="Theme 1"></button>
<button class="theme-button" id="theme-2" data-theme="1" aria-label="Theme 2"></button>
<button class="theme-button" id="theme-3" data-theme="2" aria-label="Theme 3"></button>
<button class="theme-button" id="theme-4" data-theme="3" aria-label="Theme 4"></button>
</div>
<div id="density-controls">
<div class="density-label"><span>Density</span><span id="density-value">100%</span></div>
<input type="range" min="20" max="100" value="100" class="density-slider" id="density-slider" aria-label="Network Density">
</div>
</div>
<div id="control-buttons">
<button id="change-formation-btn" class="control-button">Formation</button>
<button id="pause-play-btn" class="control-button">Pause</button>
<button id="reset-camera-btn" class="control-button">Reset Cam</button>
</div>
<canvas id="neural-network-canvas"></canvas>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.162.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.162.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { FilmPass } from 'three/addons/postprocessing/FilmPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
const config = {
paused: false,
activePaletteIndex: 1,
currentFormation: 0,
numFormations: 4,
densityFactor: 1
};
const colorPalettes = [
[new THREE.Color(0x4F46E5), new THREE.Color(0x7C3AED), new THREE.Color(0xC026D3), new THREE.Color(0xDB2777), new THREE.Color(0x8B5CF6)],
[new THREE.Color(0xF59E0B), new THREE.Color(0xF97316), new THREE.Color(0xDC2626), new THREE.Color(0x7F1D1D), new THREE.Color(0xFBBF24)],
[new THREE.Color(0xEC4899), new THREE.Color(0x8B5CF6), new THREE.Color(0x6366F1), new THREE.Color(0x3B82F6), new THREE.Color(0xA855F7)],
[new THREE.Color(0x10B981), new THREE.Color(0xA3E635), new THREE.Color(0xFACC15), new THREE.Color(0xFB923C), new THREE.Color(0x4ADE80)]
];
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x000000, 0.0015);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1200);
camera.position.set(0, 5, 22);
const canvasElement = document.getElementById('neural-network-canvas'); // Get canvas element
const renderer = new THREE.WebGLRenderer({ canvas: canvasElement, antialias: true, powerPreference: "high-performance" });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setClearColor(0x000000);
renderer.outputColorSpace = THREE.SRGBColorSpace;
function createStarfield() {
const count = 5000, pos = [];
for (let i = 0; i < count; i++) {
const r = THREE.MathUtils.randFloat(40, 120);
const phi = Math.acos(THREE.MathUtils.randFloatSpread(2));
const theta = THREE.MathUtils.randFloat(0, Math.PI * 2);
pos.push(
r * Math.sin(phi) * Math.cos(theta),
r * Math.sin(phi) * Math.sin(theta),
r * Math.cos(phi)
);
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
const mat = new THREE.PointsMaterial({
color: 0xffffff,
size: 0.15,
sizeAttenuation: true,
depthWrite: false,
opacity: 0.8,
transparent: true
});
return new THREE.Points(geo, mat);
}
const starField = createStarfield();
scene.add(starField);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.rotateSpeed = 0.5;
controls.minDistance = 5;
controls.maxDistance = 100;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.15;
controls.enablePan = false;
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.68);
composer.addPass(bloomPass);
const filmPass = new FilmPass(0.35, 0.55, 2048, false);
composer.addPass(filmPass);
composer.addPass(new OutputPass());
const pulseUniforms = {
uTime: { value: 0.0 },
uPulsePositions: { value: [new THREE.Vector3(1e3, 1e3, 1e3), new THREE.Vector3(1e3, 1e3, 1e3), new THREE.Vector3(1e3, 1e3, 1e3)] },
uPulseTimes: { value: [-1e3, -1e3, -1e3] },
uPulseColors: { value: [new THREE.Color(1, 1, 1), new THREE.Color(1, 1, 1), new THREE.Color(1, 1, 1)] },
uPulseSpeed: { value: 15.0 },
uBaseNodeSize: { value: 0.5 },
uActivePalette: { value: 0 }
};
const noiseFunctions = `
vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}
vec4 mod289(vec4 x){return x-floor(x*(1.0/289.0))*289.0;}
vec4 permute(vec4 x){return mod289(((x*34.0)+1.0)*x);}
vec4 taylorInvSqrt(vec4 r){return 1.79284291400159-0.85373472095314*r;}
float snoise(vec3 v){
const vec2 C=vec2(1.0/6.0,1.0/3.0);const vec4 D=vec4(0.0,0.5,1.0,2.0);
vec3 i=floor(v+dot(v,C.yyy));vec3 x0=v-i+dot(i,C.xxx);vec3 g=step(x0.yzx,x0.xyz);
vec3 l=1.0-g;vec3 i1=min(g.xyz,l.zxy);vec3 i2=max(g.xyz,l.zxy);
vec3 x1=x0-i1+C.xxx;vec3 x2=x0-i2+C.yyy;vec3 x3=x0-D.yyy;i=mod289(i);
vec4 p=permute(permute(permute(i.z+vec4(0.0,i1.z,i2.z,1.0))+i.y+vec4(0.0,i1.y,i2.y,1.0))+i.x+vec4(0.0,i1.x,i2.x,1.0));
float n_=0.142857142857;vec3 ns=n_*D.wyz-D.xzx;
vec4 j=p-49.0*floor(p*ns.z*ns.z);vec4 x_=floor(j*ns.z);vec4 y_=floor(j-7.0*x_);
vec4 x=x_*ns.x+ns.yyyy;vec4 y=y_*ns.x+ns.yyyy;vec4 h=1.0-abs(x)-abs(y);
vec4 b0=vec4(x.xy,y.xy);vec4 b1=vec4(x.zw,y.zw);vec4 s0=floor(b0)*2.0+1.0;vec4 s1=floor(b1)*2.0+1.0;
vec4 sh=-step(h,vec4(0.0));vec4 a0=b0.xzyw+s0.xzyw*sh.xxyy;vec4 a1=b1.xzyw+s1.xzyw*sh.zzww;
vec3 p0=vec3(a0.xy,h.x);vec3 p1=vec3(a0.zw,h.y);vec3 p2=vec3(a1.xy,h.z);vec3 p3=vec3(a1.zw,h.w);
vec4 norm=taylorInvSqrt(vec4(dot(p0,p0),dot(p1,p1),dot(p2,p2),dot(p3,p3)));
p0*=norm.x;p1*=norm.y;p2*=norm.z;p3*=norm.w;vec4 m=max(0.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.0);
m*=m;return 42.0*dot(m*m,vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3)));
}
float fbm(vec3 p,float time){
float value=0.0;float amplitude=0.5;float frequency=1.0;int octaves=3;
for(int i=0;i<octaves;i++){
value+=amplitude*snoise(p*frequency+time*0.2*frequency);
amplitude*=0.5;frequency*=2.0;
}
return value;
}`;
const nodeShader = {
vertexShader: `${noiseFunctions}
attribute float nodeSize;attribute float nodeType;attribute vec3 nodeColor;attribute vec3 connectionIndices;attribute float distanceFromRoot;
uniform float uTime;uniform vec3 uPulsePositions[3];uniform float uPulseTimes[3];uniform float uPulseSpeed;uniform float uBaseNodeSize;
varying vec3 vColor;varying float vNodeType;varying vec3 vPosition;varying float vPulseIntensity;varying float vDistanceFromRoot;
float getPulseIntensity(vec3 worldPos, vec3 pulsePos, float pulseTime) {
if (pulseTime < 0.0) return 0.0;
float timeSinceClick = uTime - pulseTime;
if (timeSinceClick < 0.0 || timeSinceClick > 3.0) return 0.0;
float pulseRadius = timeSinceClick * uPulseSpeed;
float distToClick = distance(worldPos, pulsePos);
float pulseThickness = 2.0;
float waveProximity = abs(distToClick - pulseRadius);
return smoothstep(pulseThickness, 0.0, waveProximity) * smoothstep(3.0, 0.0, timeSinceClick);
}
void main() {
vNodeType = nodeType;
vColor = nodeColor;
vDistanceFromRoot = distanceFromRoot;
vec3 worldPos = (modelMatrix * vec4(position, 1.0)).xyz;
vPosition = worldPos;
float totalPulseIntensity = 0.0;
for (int i = 0; i < 3; i++) {
totalPulseIntensity += getPulseIntensity(worldPos, uPulsePositions[i], uPulseTimes[i]);
}
vPulseIntensity = min(totalPulseIntensity, 1.0);
float timeScale = 0.5 + 0.5 * sin(uTime * 0.8 + distanceFromRoot * 0.2);
float baseSize = nodeSize * (0.8 + 0.2 * timeScale);
float pulseSize = baseSize * (1.0 + vPulseIntensity * 2.0);
vec3 modifiedPosition = position;
if (nodeType > 0.5) {
float noise = fbm(position * 0.1, uTime * 0.1);
modifiedPosition += normal * noise * 0.2;
}
vec4 mvPosition = modelViewMatrix * vec4(modifiedPosition, 1.0);
gl_PointSize = pulseSize * uBaseNodeSize * (800.0 / -mvPosition.z);
gl_Position = projectionMatrix * mvPosition;
}`,
fragmentShader: `
uniform float uTime;uniform vec3 uPulseColors[3];uniform int uActivePalette;
varying vec3 vColor;varying float vNodeType;varying vec3 vPosition;varying float vPulseIntensity;varying float vDistanceFromRoot;
void main() {
vec2 center = 2.0 * gl_PointCoord - 1.0;
float dist = length(center);
if (dist > 1.0) discard;
float glowStrength = 1.0 - smoothstep(0.0, 1.0, dist);
glowStrength = pow(glowStrength, 1.4);
vec3 baseColor = vColor * (0.8 + 0.2 * sin(uTime * 0.5 + vDistanceFromRoot * 0.3));
vec3 finalColor = baseColor;
if (vPulseIntensity > 0.0) {
vec3 pulseColor = mix(vec3(1.0), uPulseColors[0], 0.3);
finalColor = mix(baseColor, pulseColor, vPulseIntensity);
finalColor *= (1.0 + vPulseIntensity * 0.7);
}
float alpha = glowStrength * (0.9 - 0.5 * dist);
float camDistance = length(vPosition - cameraPosition);
float distanceFade = smoothstep(80.0, 10.0, camDistance);
if (vNodeType > 0.5) {
alpha *= 0.85;
} else {
finalColor *= 1.2;
}
gl_FragColor = vec4(finalColor, alpha * distanceFade);
}`
};
const connectionShader = {
vertexShader: `${noiseFunctions}
attribute vec3 startPoint;attribute vec3 endPoint;attribute float connectionStrength;attribute float pathIndex;attribute vec3 connectionColor;
uniform float uTime;uniform vec3 uPulsePositions[3];uniform float uPulseTimes[3];uniform float uPulseSpeed;
varying vec3 vColor;varying float vConnectionStrength;varying float vPulseIntensity;varying float vPathPosition;
float getPulseIntensity(vec3 worldPos, vec3 pulsePos, float pulseTime) {
if (pulseTime < 0.0) return 0.0;
float timeSinceClick = uTime - pulseTime;
if (timeSinceClick < 0.0 || timeSinceClick > 3.0) return 0.0;
float pulseRadius = timeSinceClick * uPulseSpeed;
float distToClick = distance(worldPos, pulsePos);
float pulseThickness = 2.0;
float waveProximity = abs(distToClick - pulseRadius);
return smoothstep(pulseThickness, 0.0, waveProximity) * smoothstep(3.0, 0.0, timeSinceClick);
}
void main() {
float t = position.x;
vPathPosition = t;
vec3 midPoint = mix(startPoint, endPoint, 0.5);
float pathOffset = sin(t * 3.14159) * 0.1;
vec3 perpendicular = normalize(cross(normalize(endPoint - startPoint), vec3(0.0, 1.0, 0.0)));
if (length(perpendicular) < 0.1) perpendicular = vec3(1.0, 0.0, 0.0);
midPoint += perpendicular * pathOffset;
vec3 p0 = mix(startPoint, midPoint, t);
vec3 p1 = mix(midPoint, endPoint, t);
vec3 finalPos = mix(p0, p1, t);
float noiseTime = uTime * 0.2;
float noise = fbm(vec3(pathIndex * 0.1, t * 0.5, noiseTime), noiseTime);
finalPos += perpendicular * noise * 0.1;
vec3 worldPos = (modelMatrix * vec4(finalPos, 1.0)).xyz;
float totalPulseIntensity = 0.0;
for (int i = 0; i < 3; i++) {
totalPulseIntensity += getPulseIntensity(worldPos, uPulsePositions[i], uPulseTimes[i]);
}
vPulseIntensity = min(totalPulseIntensity, 1.0);
vColor = connectionColor;
vConnectionStrength = connectionStrength;
gl_Position = projectionMatrix * modelViewMatrix * vec4(finalPos, 1.0);
}`,
fragmentShader: `
uniform float uTime;uniform vec3 uPulseColors[3];
varying vec3 vColor;varying float vConnectionStrength;varying float vPulseIntensity;varying float vPathPosition;
void main() {
vec3 baseColor = vColor * (0.7 + 0.3 * sin(uTime * 0.5 + vPathPosition * 10.0));
float flowPattern = sin(vPathPosition * 20.0 - uTime * 3.0) * 0.5 + 0.5;
float flowIntensity = 0.3 * flowPattern * vConnectionStrength;
vec3 finalColor = baseColor;
if (vPulseIntensity > 0.0) {
vec3 pulseColor = mix(vec3(1.0), uPulseColors[0], 0.3);
finalColor = mix(baseColor, pulseColor, vPulseIntensity);
flowIntensity += vPulseIntensity * 0.5;
}
finalColor *= (0.6 + flowIntensity + vConnectionStrength * 0.4);
float alpha = 0.8 * vConnectionStrength + 0.2 * flowPattern;
alpha = mix(alpha, min(1.0, alpha * 2.0), vPulseIntensity);
gl_FragColor = vec4(finalColor, alpha);
}`
};
class Node {
constructor(position, level = 0, type = 0) {
this.position = position;
this.connections = [];
this.level = level;
this.type = type;
this.size = type === 0 ? THREE.MathUtils.randFloat(0.7, 1.2) : THREE.MathUtils.randFloat(0.4, 0.9);
this.distanceFromRoot = 0;
}
addConnection(node, strength = 1.0) {
if (!this.isConnectedTo(node)) {
this.connections.push({ node, strength });
node.connections.push({ node: this, strength });
}
}
isConnectedTo(node) {
return this.connections.some(conn => conn.node === node);
}
}
function generateNeuralNetwork(formationIndex, densityFactor = 1.0) {
let nodes = [];
let rootNode;
function generateQuantumCortex() {
rootNode = new Node(new THREE.Vector3(0, 0, 0), 0, 0); rootNode.size = 1.5; nodes.push(rootNode);
const layers = 5, primaryAxes = 6, nodesPerAxis = 8, axisLength = 20;
const axisEndpoints = [];
for (let a = 0; a < primaryAxes; a++) {
const phi = Math.acos(-1 + (2 * a) / primaryAxes);
const theta = Math.PI * (1 + Math.sqrt(5)) * a;
const dirVec = new THREE.Vector3(
Math.sin(phi) * Math.cos(theta),
Math.sin(phi) * Math.sin(theta),
Math.cos(phi)
);
let prevNode = rootNode;
for (let i = 1; i <= nodesPerAxis; i++) {
const t = i / nodesPerAxis;
const distance = axisLength * Math.pow(t, 0.8);
const pos = new THREE.Vector3().copy(dirVec).multiplyScalar(distance);
const nodeType = (i === nodesPerAxis) ? 1 : 0;
const newNode = new Node(pos, i, nodeType);
newNode.distanceFromRoot = distance;
nodes.push(newNode);
prevNode.addConnection(newNode, 1.0 - (t * 0.3));
prevNode = newNode;
if (i === nodesPerAxis) axisEndpoints.push(newNode);
}
}
const ringDistances = [5, 10, 15];
const ringNodes = [];
for (const ringDist of ringDistances) {
const nodesInRing = Math.floor(ringDist * 3 * densityFactor);
const ringLayer = [];
for (let i = 0; i < nodesInRing; i++) {
const t = i / nodesInRing;
const ringPhi = Math.acos(2 * Math.random() - 1);
const ringTheta = 2 * Math.PI * t;
const pos = new THREE.Vector3(
ringDist * Math.sin(ringPhi) * Math.cos(ringTheta),
ringDist * Math.sin(ringPhi) * Math.sin(ringTheta),
ringDist * Math.cos(ringPhi)
);
const level = Math.ceil(ringDist / 5);
const nodeType = Math.random() < 0.4 ? 1 : 0;
const newNode = new Node(pos, level, nodeType);
newNode.distanceFromRoot = ringDist;
nodes.push(newNode);
ringLayer.push(newNode);
}
ringNodes.push(ringLayer);
for (let i = 0; i < ringLayer.length; i++) {
const node = ringLayer[i];
const nextNode = ringLayer[(i + 1) % ringLayer.length];
node.addConnection(nextNode, 0.7);
if (i % 4 === 0 && ringLayer.length > 5) {
const jumpIdx = (i + Math.floor(ringLayer.length / 2)) % ringLayer.length;
node.addConnection(ringLayer[jumpIdx], 0.4);
}
}
}
for (const ring of ringNodes) {
for (const node of ring) {
let closestAxisNode = null; let minDist = Infinity;
for (const n of nodes) {
if (n === rootNode || n === node) continue;
if (n.level === 0 || n.type !== 0) continue;
const dist = node.position.distanceTo(n.position);
if (dist < minDist) { minDist = dist; closestAxisNode = n; }
}
if (closestAxisNode && minDist < 8) {
const strength = 0.5 + (1 - minDist / 8) * 0.5;
node.addConnection(closestAxisNode, strength);
}
}
}
for (let r = 0; r < ringNodes.length - 1; r++) {
const innerRing = ringNodes[r];
const outerRing = ringNodes[r + 1];
const connectionsCount = Math.floor(innerRing.length * 0.5);
for (let i = 0; i < connectionsCount; i++) {
const innerNode = innerRing[Math.floor(Math.random() * innerRing.length)];
const outerNode = outerRing[Math.floor(Math.random() * outerRing.length)];
if (!innerNode.isConnectedTo(outerNode)) {
innerNode.addConnection(outerNode, 0.6);
}
}
}
for (let i = 0; i < axisEndpoints.length; i++) {
const startNode = axisEndpoints[i];
const endNode = axisEndpoints[(i + 2) % axisEndpoints.length];
const numIntermediates = 3;
let prevNode = startNode;
for (let j = 1; j <= numIntermediates; j++) {
const t = j / (numIntermediates + 1);
const pos = new THREE.Vector3().lerpVectors(startNode.position, endNode.position, t);
pos.add(new THREE.Vector3(
THREE.MathUtils.randFloatSpread(3),
THREE.MathUtils.randFloatSpread(3),
THREE.MathUtils.randFloatSpread(3)
));
const newNode = new Node(pos, startNode.level, 0);
newNode.distanceFromRoot = rootNode.position.distanceTo(pos);
nodes.push(newNode);
prevNode.addConnection(newNode, 0.5);
prevNode = newNode;
}
prevNode.addConnection(endNode, 0.5);
}
}
function generateHyperdimensionalMesh() {
rootNode = new Node(new THREE.Vector3(0, 0, 0), 0, 0); rootNode.size = 1.5; nodes.push(rootNode);
const dimensions = 4;
const nodesPerDimension = Math.floor(40 * densityFactor);
const maxRadius = 20;
const dimensionVectors = [
new THREE.Vector3(1, 1, 1).normalize(),
new THREE.Vector3(-1, 1, -1).normalize(),
new THREE.Vector3(1, -1, -1).normalize(),
new THREE.Vector3(-1, -1, 1).normalize()
];
const dimensionNodes = [];
for (let d = 0; d < dimensions; d++) {
const dimNodes = [];
const dimVec = dimensionVectors[d];
for (let i = 0; i < nodesPerDimension; i++) {
const distance = maxRadius * Math.pow(Math.random(), 0.7);
const randomVec = new THREE.Vector3(
THREE.MathUtils.randFloatSpread(1),
THREE.MathUtils.randFloatSpread(1),
THREE.MathUtils.randFloatSpread(1)
).normalize();
const biasedVec = new THREE.Vector3().addVectors(
dimVec.clone().multiplyScalar(0.6 + Math.random() * 0.4),
randomVec.clone().multiplyScalar(0.3)
).normalize();
const pos = biasedVec.clone().multiplyScalar(distance);
const isLeaf = Math.random() < 0.4 || distance > maxRadius * 0.8;
const level = Math.floor(distance / (maxRadius / 4)) + 1;
const newNode = new Node(pos, level, isLeaf ? 1 : 0);
newNode.distanceFromRoot = distance;
newNode.dimension = d;
nodes.push(newNode);
dimNodes.push(newNode);
if (distance < maxRadius * 0.3) rootNode.addConnection(newNode, 0.7);
}
dimensionNodes.push(dimNodes);
}
for (let d = 0; d < dimensions; d++) {
const dimNodes = dimensionNodes[d];
dimNodes.sort((a, b) => a.distanceFromRoot - b.distanceFromRoot);
const layers = 4;
const nodesPerLayer = Math.ceil(dimNodes.length / layers);
for (let layer = 0; layer < layers; layer++) {
const startIdx = layer * nodesPerLayer;
const endIdx = Math.min(startIdx + nodesPerLayer, dimNodes.length);
for (let i = startIdx; i < endIdx; i++) {
const node = dimNodes[i];
const connectionsCount = 1 + Math.floor(Math.random() * 3);
const nearbyNodes = dimNodes.slice(startIdx, endIdx).filter(n => n !== node)
.sort((a, b) => node.position.distanceTo(a.position) - node.position.distanceTo(b.position));
for (let j = 0; j < Math.min(connectionsCount, nearbyNodes.length); j++) {
if (!node.isConnectedTo(nearbyNodes[j])) {
node.addConnection(nearbyNodes[j], 0.4 + Math.random() * 0.4);
}
}
if (layer > 0) {
const prevLayer = dimNodes.slice((layer - 1) * nodesPerLayer, layer * nodesPerLayer)
.sort((a, b) => node.position.distanceTo(a.position) - node.position.distanceTo(b.position));
if (prevLayer.length > 0 && !node.isConnectedTo(prevLayer[0])) {
node.addConnection(prevLayer[0], 0.8);
}
}
}
}
}
for (let d1 = 0; d1 < dimensions; d1++) {
for (let d2 = d1 + 1; d2 < dimensions; d2++) {
const connectionsCount = Math.floor(5 * densityFactor);
for (let i = 0; i < connectionsCount; i++) {
const n1 = dimensionNodes[d1][Math.floor(Math.random() * dimensionNodes[d1].length)];
const n2 = dimensionNodes[d2][Math.floor(Math.random() * dimensionNodes[d2].length)];
if (!n1.isConnectedTo(n2)) {
const midPos = new THREE.Vector3().lerpVectors(n1.position, n2.position, 0.5);
midPos.add(new THREE.Vector3(
THREE.MathUtils.randFloatSpread(2),
THREE.MathUtils.randFloatSpread(2),
THREE.MathUtils.randFloatSpread(2)
));
const interNode = new Node(midPos, Math.max(n1.level, n2.level), 0);
interNode.distanceFromRoot = rootNode.position.distanceTo(midPos);
nodes.push(interNode);
n1.addConnection(interNode, 0.5);
interNode.addConnection(n2, 0.5);
}
}
}
}
const jumpConnections = Math.floor(10 * densityFactor);
for (let i = 0; i < jumpConnections; i++) {
const startDim = Math.floor(Math.random() * dimensions);
const endDim = (startDim + 2) % dimensions;
const startNode = dimensionNodes[startDim][Math.floor(Math.random() * dimensionNodes[startDim].length)];
const endNode = dimensionNodes[endDim][Math.floor(Math.random() * dimensionNodes[endDim].length)];
if (!startNode.isConnectedTo(endNode)) {
const numPoints = 3 + Math.floor(Math.random() * 3);
let prevNode = startNode;
for (let j = 1; j < numPoints; j++) {
const t = j / numPoints;
const pos = new THREE.Vector3().lerpVectors(startNode.position, endNode.position, t);
pos.add(new THREE.Vector3(
THREE.MathUtils.randFloatSpread(8) * Math.sin(t * Math.PI),
THREE.MathUtils.randFloatSpread(8) * Math.sin(t * Math.PI),
THREE.MathUtils.randFloatSpread(8) * Math.sin(t * Math.PI)
));
const jumpNode = new Node(pos, Math.max(startNode.level, endNode.level), 0);
jumpNode.distanceFromRoot = rootNode.position.distanceTo(pos);
nodes.push(jumpNode);
prevNode.addConnection(jumpNode, 0.4);
prevNode = jumpNode;
}
prevNode.addConnection(endNode, 0.4);
}
}
}
function generateNeuralVortex() {
rootNode = new Node(new THREE.Vector3(0, 0, 0), 0, 0); rootNode.size = 1.8; nodes.push(rootNode);
const numSpirals = 6;
const totalHeight = 30;
const maxRadius = 16;
const nodesPerSpiral = Math.floor(30 * densityFactor);
const spiralNodes = [];
for (let s = 0; s < numSpirals; s++) {
const spiralPhase = (s / numSpirals) * Math.PI * 2;
const spiralArray = [];
for (let i = 0; i < nodesPerSpiral; i++) {
const t = i / (nodesPerSpiral - 1);
const heightCurve = 1 - Math.pow(2 * t - 1, 2);
const height = (t - 0.5) * totalHeight;
const radiusCurve = Math.sin(t * Math.PI);
const radius = maxRadius * radiusCurve;
const revolutions = 2.5;
const angle = spiralPhase + t * Math.PI * 2 * revolutions;
const pos = new THREE.Vector3(radius * Math.cos(angle), height, radius * Math.sin(angle));
pos.add(new THREE.Vector3(
THREE.MathUtils.randFloatSpread(1.5),
THREE.MathUtils.randFloatSpread(1.5),
THREE.MathUtils.randFloatSpread(1.5)
));
const level = Math.floor(t * 5) + 1;
const isLeaf = Math.random() < 0.3 || i > nodesPerSpiral - 3;
const newNode = new Node(pos, level, isLeaf ? 1 : 0);
newNode.distanceFromRoot = Math.sqrt(radius * radius + height * height);
newNode.spiralIndex = s;
newNode.spiralPosition = t;
nodes.push(newNode);
spiralArray.push(newNode);
}
spiralNodes.push(spiralArray);
}
for (const spiral of spiralNodes) {
rootNode.addConnection(spiral[0], 1.0);
for (let i = 0; i < spiral.length - 1; i++) {
spiral[i].addConnection(spiral[i + 1], 0.9);
}
}
for (let s = 0; s < numSpirals; s++) {
const currentSpiral = spiralNodes[s];
const nextSpiral = spiralNodes[(s + 1) % numSpirals];
const connectionPoints = 5;
for (let c = 0; c < connectionPoints; c++) {
const t = c / (connectionPoints - 1);
const idx1 = Math.floor(t * (currentSpiral.length - 1));
const idx2 = Math.floor(t * (nextSpiral.length - 1));
currentSpiral[idx1].addConnection(nextSpiral[idx2], 0.7);
}
}
for (let s = 0; s < numSpirals; s++) {
const currentSpiral = spiralNodes[s];
const jumpSpiral = spiralNodes[(s + 2) % numSpirals];
const connections = 3;
for (let c = 0; c < connections; c++) {
const t1 = (c + 0.5) / connections;
const t2 = (c + 1.0) / connections;
const idx1 = Math.floor(t1 * (currentSpiral.length - 1));
const idx2 = Math.floor(t2 * (jumpSpiral.length - 1));
const start = currentSpiral[idx1];
const end = jumpSpiral[idx2];
const midPoint = new THREE.Vector3().lerpVectors(start.position, end.position, 0.5).multiplyScalar(0.7);
const bridgeNode = new Node(midPoint, Math.max(start.level, end.level), 0);
bridgeNode.distanceFromRoot = rootNode.position.distanceTo(midPoint);
nodes.push(bridgeNode);
start.addConnection(bridgeNode, 0.6);
bridgeNode.addConnection(end, 0.6);
}
}
const ringLevels = 5;
for (let r = 0; r < ringLevels; r++) {
const height = (r / (ringLevels - 1) - 0.5) * totalHeight * 0.7;
const ringNodes = nodes.filter(n => n !== rootNode && Math.abs(n.position.y - height) < 2);
ringNodes.sort((a, b) => Math.atan2(a.position.z, a.position.x) - Math.atan2(b.position.z, b.position.x));
if (ringNodes.length > 3) {
for (let i = 0; i < ringNodes.length; i++) {
ringNodes[i].addConnection(ringNodes[(i + 1) % ringNodes.length], 0.5);
}
}
}
const radialConnections = Math.floor(10 * densityFactor);
const candidates = nodes.filter(n => n !== rootNode && n.position.length() > 5)
.sort(() => Math.random() - 0.5)
.slice(0, radialConnections);
for (const node of candidates) {
const numSegments = 1 + Math.floor(Math.random() * 2);
let prevNode = node;
for (let i = 1; i <= numSegments; i++) {
const t = i / (numSegments + 1);
const segPos = node.position.clone().multiplyScalar(1 - t);
segPos.add(new THREE.Vector3(
THREE.MathUtils.randFloatSpread(2),
THREE.MathUtils.randFloatSpread(2),
THREE.MathUtils.randFloatSpread(2)
));
const newNode = new Node(segPos, Math.floor(node.level * (1 - t)), 0);
newNode.distanceFromRoot = rootNode.position.distanceTo(segPos);
nodes.push(newNode);
prevNode.addConnection(newNode, 0.7);
prevNode = newNode;
}
prevNode.addConnection(rootNode, 0.8);
}
}
function generateSynapticCloud() {
rootNode = new Node(new THREE.Vector3(0, 0, 0), 0, 0); rootNode.size = 1.5; nodes.push(rootNode);
const numClusters = 6;
const maxDist = 18;
const clusterNodes = [];
for (let c = 0; c < numClusters; c++) {
const phi = Math.acos(2 * Math.random() - 1);
const theta = 2 * Math.PI * Math.random();
const distance = maxDist * (0.3 + 0.7 * Math.random());
const pos = new THREE.Vector3(
distance * Math.sin(phi) * Math.cos(theta),
distance * Math.sin(phi) * Math.sin(theta),
distance * Math.cos(phi)
);
const clusterNode = new Node(pos, 1, 0);
clusterNode.size = 1.2;
clusterNode.distanceFromRoot = distance;
nodes.push(clusterNode);
clusterNodes.push(clusterNode);
rootNode.addConnection(clusterNode, 0.9);
}
for (let i = 0; i < clusterNodes.length; i++) {
for (let j = i + 1; j < clusterNodes.length; j++) {
const dist = clusterNodes[i].position.distanceTo(clusterNodes[j].position);
const probability = 1.0 - (dist / (maxDist * 2));
if (Math.random() < probability) {
const strength = 0.5 + 0.5 * (1 - dist / (maxDist * 2));
clusterNodes[i].addConnection(clusterNodes[j], strength);
}
}
}
for (const cluster of clusterNodes) {
const clusterSize = Math.floor(20 * densityFactor);
const cloudRadius = 7 + Math.random() * 3;
for (let i = 0; i < clusterSize; i++) {
const radius = cloudRadius * Math.pow(Math.random(), 0.5);
const dir = new THREE.Vector3(
THREE.MathUtils.randFloatSpread(2),
THREE.MathUtils.randFloatSpread(2),
THREE.MathUtils.randFloatSpread(2)
).normalize();
const pos = new THREE.Vector3().copy(cluster.position).add(dir.multiplyScalar(radius));
const distanceFromCluster = radius;
const distanceFromRoot = rootNode.position.distanceTo(pos);
const level = 2 + Math.floor(distanceFromCluster / 3);
const isLeaf = Math.random() < 0.5;
const newNode = new Node(pos, level, isLeaf ? 1 : 0);
newNode.distanceFromRoot = distanceFromRoot;
newNode.clusterRef = cluster;
nodes.push(newNode);
const strength = 0.7 * (1 - distanceFromCluster / cloudRadius);
cluster.addConnection(newNode, strength);
const nearbyNodes = nodes.filter(n =>
n !== newNode && n !== cluster && n.clusterRef === cluster &&
n.position.distanceTo(pos) < cloudRadius * 0.4
);
const connectionsCount = Math.floor(Math.random() * 3);
nearbyNodes.sort((a, b) => pos.distanceTo(a.position) - pos.distanceTo(b.position));
for (let j = 0; j < Math.min(connectionsCount, nearbyNodes.length); j++) {
const dist = pos.distanceTo(nearbyNodes[j].position);
const connStrength = 0.4 * (1 - dist / (cloudRadius * 0.4));
newNode.addConnection(nearbyNodes[j], connStrength);
}
}
}
const interClusterCount = Math.floor(15 * densityFactor);
for (let i = 0; i < interClusterCount; i++) {
const cluster1 = clusterNodes[Math.floor(Math.random() * clusterNodes.length)];
let cluster2;
do { cluster2 = clusterNodes[Math.floor(Math.random() * clusterNodes.length)]; } while (cluster2 === cluster1);
const bridgePos = new THREE.Vector3().lerpVectors(cluster1.position, cluster2.position, 0.3 + Math.random() * 0.4);
bridgePos.add(new THREE.Vector3(
THREE.MathUtils.randFloatSpread(5),
THREE.MathUtils.randFloatSpread(5),
THREE.MathUtils.randFloatSpread(5)
));
const bridgeNode = new Node(bridgePos, 2, 0);
bridgeNode.distanceFromRoot = rootNode.position.distanceTo(bridgePos);
nodes.push(bridgeNode);
cluster1.addConnection(bridgeNode, 0.5);
cluster2.addConnection(bridgeNode, 0.5);
const nearbyNodes = nodes.filter(n => n !== bridgeNode && n !== cluster1 && n !== cluster2 && n.position.distanceTo(bridgePos) < 8);
if (nearbyNodes.length > 0) {
const target = nearbyNodes[Math.floor(Math.random() * nearbyNodes.length)];
bridgeNode.addConnection(target, 0.4);
}
}
const longRangeCount = Math.floor(10 * densityFactor);
const outerNodes = nodes.filter(n => n.distanceFromRoot > maxDist * 0.6)
.sort(() => Math.random() - 0.5)
.slice(0, longRangeCount);
for (const outerNode of outerNodes) {
const numSegments = 2 + Math.floor(Math.random() * 2);
let prevNode = outerNode;
for (let i = 1; i <= numSegments; i++) {
const t = i / (numSegments + 1);
const segPos = outerNode.position.clone().multiplyScalar(1 - t * 0.8);
segPos.add(new THREE.Vector3(
THREE.MathUtils.randFloatSpread(4),
THREE.MathUtils.randFloatSpread(4),
THREE.MathUtils.randFloatSpread(4)
));
const newNode = new Node(segPos, outerNode.level, 0);
newNode.distanceFromRoot = rootNode.position.distanceTo(segPos);
nodes.push(newNode);
prevNode.addConnection(newNode, 0.6);
prevNode = newNode;
}
const innerNodes = nodes.filter(n => n.distanceFromRoot < maxDist * 0.4 && n !== rootNode);
if (innerNodes.length > 0) {
const targetNode = innerNodes[Math.floor(Math.random() * innerNodes.length)];
prevNode.addConnection(targetNode, 0.5);
}
}
}
switch (formationIndex % 4) {
case 0: generateQuantumCortex(); break;
case 1: generateHyperdimensionalMesh(); break;
case 2: generateNeuralVortex(); break;
case 3: generateSynapticCloud(); break;
}
if (densityFactor < 1.0) {
const originalNodeCount = nodes.length;
nodes = nodes.filter((node, index) => {
if (node === rootNode) return true;
const hash = (index * 31 + Math.floor(densityFactor * 100)) % 100;
return hash < (densityFactor * 100);
});
nodes.forEach(node => {
node.connections = node.connections.filter(conn => nodes.includes(conn.node));
});
console.log(`Density Filter: ${originalNodeCount} -> ${nodes.length} nodes`);
}
return { nodes, rootNode };
}
let neuralNetwork = null, nodesMesh = null, connectionsMesh = null;
function createNetworkVisualization(formationIndex, densityFactor = 1.0) {
console.log(`Creating formation ${formationIndex}, density ${densityFactor}`);
if (nodesMesh) {
scene.remove(nodesMesh);
nodesMesh.geometry.dispose();
nodesMesh.material.dispose();
nodesMesh = null;
}
if (connectionsMesh) {
scene.remove(connectionsMesh);
connectionsMesh.geometry.dispose();
connectionsMesh.material.dispose();
connectionsMesh = null;
}
neuralNetwork = generateNeuralNetwork(formationIndex, densityFactor);
if (!neuralNetwork || neuralNetwork.nodes.length === 0) {
console.error("Network generation failed or resulted in zero nodes.");
return;
}
const nodesGeometry = new THREE.BufferGeometry();
const nodePositions = [], nodeTypes = [], nodeSizes = [], nodeColors = [], connectionIndices = [], distancesFromRoot = [];
neuralNetwork.nodes.forEach((node, index) => {
nodePositions.push(node.position.x, node.position.y, node.position.z);
nodeTypes.push(node.type);
nodeSizes.push(node.size);
distancesFromRoot.push(node.distanceFromRoot);
const indices = node.connections.slice(0, 3).map(conn => neuralNetwork.nodes.indexOf(conn.node));
while (indices.length < 3) indices.push(-1);
connectionIndices.push(...indices);
const palette = colorPalettes[config.activePaletteIndex];
const colorIndex = Math.min(node.level, palette.length - 1);
const baseColor = palette[colorIndex % palette.length].clone();
baseColor.offsetHSL(
THREE.MathUtils.randFloatSpread(0.05),
THREE.MathUtils.randFloatSpread(0.1),
THREE.MathUtils.randFloatSpread(0.1)
);
nodeColors.push(baseColor.r, baseColor.g, baseColor.b);
});
nodesGeometry.setAttribute('position', new THREE.Float32BufferAttribute(nodePositions, 3));
nodesGeometry.setAttribute('nodeType', new THREE.Float32BufferAttribute(nodeTypes, 1));
nodesGeometry.setAttribute('nodeSize', new THREE.Float32BufferAttribute(nodeSizes, 1));
nodesGeometry.setAttribute('nodeColor', new THREE.Float32BufferAttribute(nodeColors, 3));
nodesGeometry.setAttribute('connectionIndices', new THREE.Float32BufferAttribute(connectionIndices, 3));
nodesGeometry.setAttribute('distanceFromRoot', new THREE.Float32BufferAttribute(distancesFromRoot, 1));
const nodesMaterial = new THREE.ShaderMaterial({
uniforms: THREE.UniformsUtils.clone(pulseUniforms),
vertexShader: nodeShader.vertexShader,
fragmentShader: nodeShader.fragmentShader,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending
});
nodesMesh = new THREE.Points(nodesGeometry, nodesMaterial);
scene.add(nodesMesh);
const connectionsGeometry = new THREE.BufferGeometry();
const connectionColors = [], connectionStrengths = [], connectionPositions = [], startPoints = [], endPoints = [], pathIndices = [];
const processedConnections = new Set();
let pathIndex = 0;
neuralNetwork.nodes.forEach((node, nodeIndex) => {
node.connections.forEach(connection => {
const connectedNode = connection.node;
const connectedIndex = neuralNetwork.nodes.indexOf(connectedNode);
if (connectedIndex === -1) return;
const key = [Math.min(nodeIndex, connectedIndex), Math.max(nodeIndex, connectedIndex)].join('-');
if (!processedConnections.has(key)) {
processedConnections.add(key);
const startPoint = node.position;
const endPoint = connectedNode.position;
const numSegments = 15;
for (let i = 0; i < numSegments; i++) {
const t = i / (numSegments - 1);
connectionPositions.push(t, 0, 0);
startPoints.push(startPoint.x, startPoint.y, startPoint.z);
endPoints.push(endPoint.x, endPoint.y, endPoint.z);
pathIndices.push(pathIndex);
connectionStrengths.push(connection.strength);
const palette = colorPalettes[config.activePaletteIndex];
const avgLevel = Math.min(Math.floor((node.level + connectedNode.level) / 2), palette.length - 1);
const baseColor = palette[avgLevel % palette.length].clone();
baseColor.offsetHSL(
THREE.MathUtils.randFloatSpread(0.05),
THREE.MathUtils.randFloatSpread(0.1),
THREE.MathUtils.randFloatSpread(0.1)
);
connectionColors.push(baseColor.r, baseColor.g, baseColor.b);
}
pathIndex++;
}
});
});
connectionsGeometry.setAttribute('position', new THREE.Float32BufferAttribute(connectionPositions, 3));
connectionsGeometry.setAttribute('startPoint', new THREE.Float32BufferAttribute(startPoints, 3));
connectionsGeometry.setAttribute('endPoint', new THREE.Float32BufferAttribute(endPoints, 3));
connectionsGeometry.setAttribute('connectionStrength', new THREE.Float32BufferAttribute(connectionStrengths, 1));
connectionsGeometry.setAttribute('connectionColor', new THREE.Float32BufferAttribute(connectionColors, 3));
connectionsGeometry.setAttribute('pathIndex', new THREE.Float32BufferAttribute(pathIndices, 1));
const connectionsMaterial = new THREE.ShaderMaterial({
uniforms: THREE.UniformsUtils.clone(pulseUniforms),
vertexShader: connectionShader.vertexShader,
fragmentShader: connectionShader.fragmentShader,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending
});
connectionsMesh = new THREE.LineSegments(connectionsGeometry, connectionsMaterial);
scene.add(connectionsMesh);
const palette = colorPalettes[config.activePaletteIndex];
connectionsMaterial.uniforms.uPulseColors.value[0].copy(palette[0]);
connectionsMaterial.uniforms.uPulseColors.value[1].copy(palette[1]);
connectionsMaterial.uniforms.uPulseColors.value[2].copy(palette[2]);
nodesMaterial.uniforms.uPulseColors.value[0].copy(palette[0]);
nodesMaterial.uniforms.uPulseColors.value[1].copy(palette[1]);
nodesMaterial.uniforms.uPulseColors.value[2].copy(palette[2]);
nodesMaterial.uniforms.uActivePalette.value = config.activePaletteIndex;
}
function updateTheme(paletteIndex) {
config.activePaletteIndex = paletteIndex;
if (!nodesMesh || !connectionsMesh) return;
const palette = colorPalettes[paletteIndex];
const nodeColorsAttr = nodesMesh.geometry.attributes.nodeColor;
const nodeLevels = neuralNetwork.nodes.map(n => n.level);
for (let i = 0; i < nodeColorsAttr.count; i++) {
const node = neuralNetwork.nodes[i];
if (!node) continue;
const colorIndex = Math.min(node.level, palette.length - 1);
const baseColor = palette[colorIndex % palette.length].clone();
baseColor.offsetHSL(
THREE.MathUtils.randFloatSpread(0.05),
THREE.MathUtils.randFloatSpread(0.1),
THREE.MathUtils.randFloatSpread(0.1)
);
nodeColorsAttr.setXYZ(i, baseColor.r, baseColor.g, baseColor.b);
}
nodeColorsAttr.needsUpdate = true;
const connectionColors = [];
const processedConnections = new Set();
neuralNetwork.nodes.forEach((node, nodeIndex) => {
node.connections.forEach(connection => {
const connectedNode = connection.node;
const connectedIndex = neuralNetwork.nodes.indexOf(connectedNode);
if (connectedIndex === -1) return;
const key = [Math.min(nodeIndex, connectedIndex), Math.max(nodeIndex, connectedIndex)].join('-');
if (!processedConnections.has(key)) {
processedConnections.add(key);
const numSegments = 15;
for (let i = 0; i < numSegments; i++) {
const avgLevel = Math.min(Math.floor((node.level + connectedNode.level) / 2), palette.length - 1);
const baseColor = palette[avgLevel % palette.length].clone();
baseColor.offsetHSL(
THREE.MathUtils.randFloatSpread(0.05),
THREE.MathUtils.randFloatSpread(0.1),
THREE.MathUtils.randFloatSpread(0.1)
);
connectionColors.push(baseColor.r, baseColor.g, baseColor.b);
}
}
});
});
connectionsMesh.geometry.setAttribute('connectionColor', new THREE.Float32BufferAttribute(connectionColors, 3));
connectionsMesh.geometry.attributes.connectionColor.needsUpdate = true;
nodesMesh.material.uniforms.uPulseColors.value.forEach((c, i) => c.copy(palette[i % palette.length]));
connectionsMesh.material.uniforms.uPulseColors.value.forEach((c, i) => c.copy(palette[i % palette.length]));
nodesMesh.material.uniforms.uActivePalette.value = paletteIndex;
}
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
const interactionPlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
const interactionPoint = new THREE.Vector3();
let lastPulseIndex = 0;
function triggerPulse(clientX, clientY) {
pointer.x = (clientX / window.innerWidth) * 2 - 1;
pointer.y = -(clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
interactionPlane.normal.copy(camera.position).normalize();
interactionPlane.constant = -interactionPlane.normal.dot(camera.position) + camera.position.length() * 0.5;
if (raycaster.ray.intersectPlane(interactionPlane, interactionPoint)) {
const time = clock.getElapsedTime();
if (nodesMesh && connectionsMesh) {
lastPulseIndex = (lastPulseIndex + 1) % 3;
nodesMesh.material.uniforms.uPulsePositions.value[lastPulseIndex].copy(interactionPoint);
nodesMesh.material.uniforms.uPulseTimes.value[lastPulseIndex] = time;
connectionsMesh.material.uniforms.uPulsePositions.value[lastPulseIndex].copy(interactionPoint);
connectionsMesh.material.uniforms.uPulseTimes.value[lastPulseIndex] = time;
const palette = colorPalettes[config.activePaletteIndex];
const randomColor = palette[Math.floor(Math.random() * palette.length)];
nodesMesh.material.uniforms.uPulseColors.value[lastPulseIndex].copy(randomColor);
connectionsMesh.material.uniforms.uPulseColors.value[lastPulseIndex].copy(randomColor);
}
}
}
renderer.domElement.addEventListener('click', (e) => {
if (e.target.closest('.ui-panel, #control-buttons')) return;
if (!config.paused) triggerPulse(e.clientX, e.clientY);
});
renderer.domElement.addEventListener('touchstart', (e) => {
if (e.target.closest('.ui-panel, #control-buttons')) return;
e.preventDefault();
if (e.touches.length > 0 && !config.paused) {
triggerPulse(e.touches[0].clientX, e.touches[0].clientY);
}
}, { passive: false });
const themeButtons = document.querySelectorAll('.theme-button');
themeButtons.forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const idx = parseInt(btn.dataset.theme, 10);
updateTheme(idx);
themeButtons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
const densitySlider = document.getElementById('density-slider');
const densityValue = document.getElementById('density-value');
let densityTimeout;
densitySlider.addEventListener('input', (e) => {
e.stopPropagation();
const val = parseInt(densitySlider.value, 10);
config.densityFactor = val / 100;
densityValue.textContent = `${val}%`;
clearTimeout(densityTimeout);
densityTimeout = setTimeout(() => {
createNetworkVisualization(config.currentFormation, config.densityFactor);
}, 300);
});
const changeFormationBtn = document.getElementById('change-formation-btn');
const pausePlayBtn = document.getElementById('pause-play-btn');
const resetCameraBtn = document.getElementById('reset-camera-btn');
changeFormationBtn.addEventListener('click', (e) => {
e.stopPropagation();
config.currentFormation = (config.currentFormation + 1) % config.numFormations;
createNetworkVisualization(config.currentFormation, config.densityFactor);
controls.autoRotate = false;
setTimeout(() => { controls.autoRotate = true; }, 2000);
});
pausePlayBtn.addEventListener('click', (e) => {
e.stopPropagation();
config.paused = !config.paused;
pausePlayBtn.textContent = config.paused ? 'Play' : 'Pause';
controls.autoRotate = !config.paused;
});
resetCameraBtn.addEventListener('click', (e) => {
e.stopPropagation();
controls.reset();
controls.autoRotate = false;
setTimeout(() => { controls.autoRotate = true; }, 1500);
});
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const t = clock.getElapsedTime();
if (!config.paused) {
if (nodesMesh) {
nodesMesh.material.uniforms.uTime.value = t;
nodesMesh.rotation.y = Math.sin(t * 0.05) * 0.08;
}
if (connectionsMesh) {
connectionsMesh.material.uniforms.uTime.value = t;
connectionsMesh.rotation.y = Math.sin(t * 0.05) * 0.08;
}
}
starField.rotation.y += 0.0003;
controls.update();
composer.render();
}
function init() {
createNetworkVisualization(config.currentFormation, config.densityFactor);
document.querySelectorAll('.theme-button').forEach(b => b.classList.remove('active'));
document.querySelector(`.theme-button[data-theme="${config.activePaletteIndex}"]`).classList.add('active');
updateTheme(config.activePaletteIndex);
animate();
}
function onWindowResize() {
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);
}
window.addEventListener('resize', onWindowResize);
init();
</script>
</body>
</html>
效果如下