学习threejs,交互式神经网络可视化

发布于:2025-06-04 ⋅ 阅读:(39) ⋅ 点赞:(0)

👨‍⚕️ 主页: 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>

效果如下
在这里插入图片描述

参考:Three.js 交互式神经网络可视化


网站公告

今日签到

点亮在社区的每一天
去签到