学习threejs,实现Transformer架构三维模拟

👨‍⚕️ 主页: gis分享者

👨‍⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!

👨‍⚕️ 收录于专栏:threejs gis工程师


文章目录

  • 一、🍀前言
    • [1.1 ☘️THREE.EffectComposer 后期处理](#1.1 ☘️THREE.EffectComposer 后期处理)
      • [1.1.1 ☘️代码示例](#1.1.1 ☘️代码示例)
      • [1.1.2 ☘️构造函数](#1.1.2 ☘️构造函数)
      • [1.1.3 ☘️属性](#1.1.3 ☘️属性)
      • [1.1.4 ☘️方法](#1.1.4 ☘️方法)
    • [1.2 ☘️THREE.RenderPass](#1.2 ☘️THREE.RenderPass)
      • [1.2.1 ☘️构造函数](#1.2.1 ☘️构造函数)
      • [1.2.2 ☘️属性](#1.2.2 ☘️属性)
      • [1.2.3 ☘️方法](#1.2.3 ☘️方法)
    • [1.3 ☘️THREE.UnrealBloomPass](#1.3 ☘️THREE.UnrealBloomPass)
      • [1.3.1 ☘️构造函数](#1.3.1 ☘️构造函数)
      • [1.3.2 ☘️方法](#1.3.2 ☘️方法)
  • 二、🍀实现Transformer架构三维模拟
    • [1. ☘️实现思路](#1. ☘️实现思路)
    • [2. ☘️代码样例](#2. ☘️代码样例)

一、🍀前言

本文详细介绍如何基于threejs在三维场景中使用EffectComposer后期处理组合器(采用RenderPass、UnrealBloomPass渲染通道),实现Transformer架构三维模拟效果,亲测可用。希望能帮助到您。一起学习,加油!加油!

1.1 ☘️THREE.EffectComposer 后期处理

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

1.1.1 ☘️代码示例

javascript 复制代码
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

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

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

.readBuffer : WebGLRenderTarget

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

.renderer : WebGLRenderer

内部渲染器的引用。

.renderToScreen : Boolean

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

.writeBuffer : WebGLRenderTarget

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

1.1.4 ☘️方法

.addPass ( pass : Pass ) : undefined

pass -- 将被添加到过程链的过程

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

.dispose () : undefined

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

.insertPass ( pass : Pass, index : Integer ) : undefined

pass -- 将被插入到过程链的过程。

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

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

.isLastEnabledPass ( passIndex : Integer ) : Boolean

passIndex -- 被用于检查的过程

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

.removePass ( pass : Pass ) : undefined

pass -- 要从传递链中删除的传递。

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

.render ( deltaTime : Float ) : undefined

deltaTime -- 增量时间值。

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

.reset ( renderTarget : WebGLRenderTarget ) : undefined

renderTarget -- (可选)一个预先配置的渲染目标,内部由 EffectComposer 使用。

重置所有EffectComposer的内部状态。

.setPixelRatio ( pixelRatio : Float ) : undefined

pixelRatio -- 设备像素比

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

.setSize ( width : Integer, height : Integer ) : undefined

width -- EffectComposer的宽度。

height -- EffectComposer的高度。

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

.swapBuffers () : undefined

交换内部的读/写缓冲。

1.2 ☘️THREE.RenderPass

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

1.2.1 ☘️构造函数

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

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

1.2.2 ☘️属性

.enabled:boolean

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

.clear:boolean

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

.needsSwap:boolean

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

1.2.3 ☘️方法

.setSize(width, height)

调整通道的渲染尺寸(通常由 EffectComposer 自动调用)。

width: 画布宽度(像素)。

height: 画布高度(像素)。

1.3 ☘️THREE.UnrealBloomPass

UnrealBloomPass 是 Three.js 中实现高质量泛光效果的后期处理通道,通过模拟类似 Unreal Engine 的泛光效果,为场景中的明亮区域添加柔和的光晕,提升视觉表现力。

1.3.1 ☘️构造函数

new UnrealBloomPass(resolution, strength, radius, threshold)

  • resolution (Vector2): 泛光效果应用的场景分辨率,需与画布尺寸一致。
    示例:new THREE.Vector2(window.innerWidth, window.innerHeight)
  • strength (Number): 泛光强度,默认值 1.0。值越大,光晕越明显。
  • radius (Number): 模糊半径,默认值 0.4。值越大,光晕扩散范围越广。
  • threshold (Number): 泛光阈值,默认值 0.85。仅对亮度高于此值的区域生效。

1.3.2 ☘️方法

  • renderToScreen: 是否直接渲染到屏幕,默认为 false(需通过 EffectComposer 管理)。
  • clearColor: 设置背景清除颜色,默认为透明。

二、🍀实现Transformer架构三维模拟

1. ☘️实现思路

这是一款纯前端实现的 Transformer 架构 3D 交互模拟,作者 Cammakingminds 在 CodePen 上发布了这个作品。打开页面后,你会看到一个沉浸式的暗黑科幻场景:左右两侧分别是 Encoder 和 Decoder 模块,数据以粒子流的形式在模块之间流动,还配有发光效果、Bloom 后处理和伪代码浮窗,视觉冲击力极强。具体代码参考下面代码样例。

2. ☘️代码样例

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Transformer架构三维模拟</title>

    <!-- Core Three.js -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script src="https://unpkg.com/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
    <script src="https://unpkg.com/three@0.128.0/examples/js/postprocessing/EffectComposer.js"></script>
    <script src="https://unpkg.com/three@0.128.0/examples/js/postprocessing/RenderPass.js"></script>
    <script src="https://unpkg.com/three@0.128.0/examples/js/postprocessing/ShaderPass.js"></script>
    <script src="https://unpkg.com/three@0.128.0/examples/js/shaders/CopyShader.js"></script>
    <script src="https://unpkg.com/three@0.128.0/examples/js/shaders/LuminosityHighPassShader.js"></script>
    <script src="https://unpkg.com/three@0.128.0/examples/js/postprocessing/UnrealBloomPass.js"></script>

    <!-- Animation -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
    <style>
        body {
            margin: 0;
            padding: 0;
            overflow: hidden;
            background-color: #020205;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            color: #fff;
        }

        canvas {
            display: block;
            width: 100vw;
            height: 100vh;
            touch-action: none;
            outline: none;
        }
    </style>
</head>
<body>
<script>
  // --- Unified Color Schema ---
  const COLORS = {
    attention: 0xff5500,
    feedForward: 0x00ffff,
    ffn: 0x00ffff,
    addNorm: 0xffff00,
    embed: 0xff0000,
    pe: 0xaaaaaa,
    softmax: 0x888888,
    crossAttention: 0xff5500,
    encoderBox: 0x00ff00,
    decoderBox: 0xff0000,

    // Particles and internal wiring
    posSine: 0x00ffff,
    posCosine: 0xff00ff,
    particle: 0xffffff,
    qLine: 0xff0055,
    kLine: 0x00f2ff,
    vLine: 0xffcc00,
    residual: 0x55aaee
  };

  const NUM_BLOCKS = 1;
  const ENCODER_X = -12;
  const DECODER_X = 12;
  const NUM_HEADS = 8;

  let scene, camera, renderer, controls, composer;
  let animatedObjects = [];
  let paths = [];
  let particles = [];
  let volumes = [];
  let particleGeometries;
  let binaryTex = [],
    asciiTex = [],
    idTex = [];

  window.activeSoftmaxAngle = 0;

  function initTextTextures() {
    const createTex = (text) => {
      const canvas = document.createElement("canvas");
      canvas.width = 64;
      canvas.height = 64;
      const ctx = canvas.getContext("2d");
      ctx.fillStyle = "#ffffff";
      ctx.font = 'bold 44px "Courier New", monospace';
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillText(text, 32, 32);
      return new THREE.CanvasTexture(canvas);
    };
    binaryTex = [createTex("0"), createTex("1")];
    const chars = [
      "A",
      "B",
      "C",
      "D",
      "E",
      "F",
      "X",
      "Y",
      "Z",
      "@",
      "#",
      "?",
      "*"
    ];
    asciiTex = chars.map((c) => createTex(c));

    const createIdTex = (text) => {
      const canvas = document.createElement("canvas");
      canvas.width = 64;
      canvas.height = 64;
      const ctx = canvas.getContext("2d");
      ctx.fillStyle = "#ffffff";
      ctx.font = 'bold 26px "Courier New", monospace';
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillText(text, 32, 32);
      return new THREE.CanvasTexture(canvas);
    };
    for (let i = 0; i < 15; i++)
      idTex.push(createIdTex(Math.floor(Math.random() * 900 + 100).toString()));
  }

  const raycaster = new THREE.Raycaster();
  const mouse = new THREE.Vector2();
  let pointerDownPos = new THREE.Vector2();
  let pointerCurrentPos = new THREE.Vector2();
  let lastTapTime = 0;
  let isPointerDown = false;
  let isHolding = false;
  let pointerDownTimestamp = 0;
  let activePointers = 0;

  const pseudoCodeData = {
    "token-embedding": {
      title: "L0: Token Embedding",
      color: "#ff0000",
      code: `<span class="keyword">def</span> <span class="function">embed</span>(input_ids):\n    <span class="comment"># Map discrete IDs to continuous vectors</span>\n    <span class="keyword">return</span> Embedding(vocab_size, d_model)(input_ids) * sqrt(d_model)`
    },
    "pos-encoding": {
      title: "L0.5: Positional Encoding",
      color: "#aaaaaa",
      code: `<span class="keyword">def</span> <span class="function">pos_encoding</span>(x):\n    <span class="comment"># Inject sequence order via phase shifts</span>\n    pe = generate_sine_cosine(seq_len, d_model)\n    <span class="keyword">return</span> x + pe`
    },
    "self-attention": {
      title: "L1: Multi-Head Self-Attention",
      color: "#ff5500",
      code: `<span class="keyword">def</span> <span class="function">multi_head_attention</span>(x, heads=<span class="keyword">8</span>):\n    Q, K, V = x@Wq, x@Wk, x@Wv\n    Q_h, K_h, V_h = split_heads(Q, K, V, heads)\n    scores = (Q_h @ K_h.T) / sqrt(d_k)\n    weights = softmax(scores, dim=-1)\n    context = weights @ V_h\n    <span class="keyword">return</span> Linear_O(concatenate(context))`
    },
    "masked-attention": {
      title: "L1-M: Masked Multi-Head Attention",
      color: "#ff5500",
      code: `<span class="keyword">def</span> <span class="function">masked_attn</span>(x, heads=<span class="keyword">8</span>):\n    Q, K, V = project_and_split(x)\n    scores = (Q @ K.T) / sqrt(d_k)\n    mask = tril(ones(seq_len, seq_len))\n    scores = scores.masked_fill(mask == <span class="keyword">0</span>, <span class="keyword">-inf</span>)\n    weights = softmax(scores, dim=-1)\n    <span class="keyword">return</span> Linear_O(weights @ V)`
    },
    "cross-attention": {
      title: "L2: Multi-Head Cross-Attention",
      color: "#ff5500",
      code: `<span class="keyword">def</span> <span class="function">cross_attn</span>(decoder_x, encoder_out):\n    Q = decoder_x @ Wq\n    K = encoder_out @ Wk\n    V = encoder_out @ Wv\n    scores = (Q @ K.T) / sqrt(d_k)\n    weights = softmax(scores, dim=-1)\n    <span class="keyword">return</span> Linear_O(weights @ V)`
    },
    "add-norm": {
      title: "L3: Add & Norm (Residual + LayerNorm)",
      color: "#ffff00",
      code: `<span class="keyword">def</span> <span class="function">add_and_norm</span>(x, sublayer_out):\n    summed = x + sublayer_out\n    x_norm = (summed - mean) / sqrt(var + eps)\n    <span class="keyword">return</span> gamma * x_norm + beta`
    },
    ffn: {
      title: "L4: Fully Connected FFN",
      color: "#00ffff",
      code: `<span class="keyword">def</span> <span class="function">ffn</span>(x):\n    <span class="comment"># Expand to high-dimensional space (4x d_model)</span>\n    h = ReLU(x @ W1 + b1)\n    <span class="comment"># Project back to base dimension</span>\n    <span class="keyword">return</span> h @ W2 + b2`
    },
    softmax: {
      title: "L5: Softmax Output",
      color: "#888888",
      code: `<span class="keyword">def</span> <span class="function">softmax_output</span>(x):\n    logits = x @ W_vocab\n    probs = softmax(logits)\n    <span class="keyword">return</span> probs`
    }
  };

  window.onload = () => {
    init();
    buildArchitecture();
    createDataFlow();
    setupInteraction();
    animate();
  };

  function init() {
    scene = new THREE.Scene();
    scene.fog = new THREE.FogExp2(0x020205, 0.006);
    camera = new THREE.PerspectiveCamera(
      45,
      window.innerWidth / window.innerHeight,
      0.1,
      2000
    );
    camera.position.set(0, 25, window.innerWidth < 768 ? 120 : 80);

    renderer = new THREE.WebGLRenderer({
      antialias: true,
      alpha: true,
      powerPreference: "high-performance"
    });
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.outputEncoding = THREE.sRGBEncoding;
    document.body.appendChild(renderer.domElement);

    const renderScene = new THREE.RenderPass(scene, camera);
    const bloomPass = new THREE.UnrealBloomPass(
      new THREE.Vector2(window.innerWidth, window.innerHeight),
      2.0,
      0.5,
      0.95
    );

    composer = new THREE.EffectComposer(renderer);
    composer.addPass(renderScene);
    composer.addPass(bloomPass);

    const gridHelper = new THREE.GridHelper(200, 100, 0x004488, 0x001122);
    gridHelper.position.y = -18;
    scene.add(gridHelper);

    scene.add(new THREE.AmbientLight(0x222233, 1.0));
    const dir = new THREE.DirectionalLight(0xffffff, 0.8);
    dir.position.set(20, 50, 30);
    scene.add(dir);
    const coreLight = new THREE.PointLight(0x00ffff, 1.5, 100);
    coreLight.position.set(0, 20, 0);
    scene.add(coreLight);

    controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.target.set(0, 20, 0);
    controls.maxDistance = 600;
    controls.autoRotateSpeed = 0.8;

    window.addEventListener("resize", () => {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
      composer.setSize(window.innerWidth, window.innerHeight);
    });

    particleGeometries = {
      cube: new THREE.BoxGeometry(0.3, 0.3, 0.3),
      sphere: new THREE.SphereGeometry(0.18, 8, 8),
      beam: new THREE.CylinderGeometry(0.04, 0.04, 0.8, 6).rotateX(Math.PI / 2),
      icosahedron: new THREE.IcosahedronGeometry(0.25, 0),
      disc: new THREE.CylinderGeometry(0.3, 0.3, 0.05, 12),
      torusKnot: new THREE.TorusKnotGeometry(0.12, 0.04, 64, 8),
      dataDart: new THREE.ConeGeometry(0.12, 0.5, 4).rotateX(Math.PI / 2)
    };
    initTextTextures();
  }

  function createGlass(color, op = 0.5) {
    return new THREE.MeshStandardMaterial({
      color,
      emissive: color,
      emissiveIntensity: 1.5,
      transparent: true,
      opacity: op
    });
  }

  function tag(group, type, volRef) {
    group.traverse((c) => {
      if (c.isMesh) {
        c.userData.blockType = type;
        if (volRef) c.userData.volumeRef = volRef;
      }
    });
  }

  function createCodePlane(type) {
    const data = pseudoCodeData[type];
    if (!data) return null;

    const canvas = document.createElement("canvas");
    canvas.width = 1024;
    canvas.height = 512;
    const ctx = canvas.getContext("2d");

    ctx.fillStyle = "rgba(5, 5, 10, 0.95)";
    ctx.fillRect(0, 0, 1024, 512);

    ctx.strokeStyle = data.color;
    ctx.lineWidth = 12;
    ctx.strokeRect(6, 6, 1012, 500);

    ctx.fillStyle = "#ffffff";
    ctx.font = 'bold 42px "Courier New", monospace';
    ctx.fillText(data.title, 40, 70);

    ctx.beginPath();
    ctx.moveTo(40, 100);
    ctx.lineTo(984, 100);
    ctx.strokeStyle = "rgba(255, 255, 255, 0.2)";
    ctx.lineWidth = 2;
    ctx.stroke();

    ctx.font = 'bold 32px "Courier New", monospace';
    const lines = data.code.split("\n");
    let y = 160;

    lines.forEach((line) => {
      let x = 40;
      const parts = line.split(/(<span class="[^"]+">.*?<\/span>)/g);
      parts.forEach((part) => {
        if (part.startsWith("<span")) {
          const match = part.match(/class="(.*?)"\s*>(.*?)</);
          if (match) {
            const cls = match[1];
            const text = match[2];
            ctx.fillStyle =
              cls === "keyword"
                ? "#ff00ff"
                : cls === "function"
                ? "#00ffff"
                : "#667788";
            ctx.fillText(text, x, y);
            x += ctx.measureText(text).width;
          }
        } else {
          ctx.fillStyle = "#aaccff";
          ctx.fillText(part, x, y);
          x += ctx.measureText(part).width;
        }
      });
      y += 45;
    });

    const tex = new THREE.CanvasTexture(canvas);
    const mat = new THREE.MeshBasicMaterial({
      map: tex,
      transparent: true,
      side: THREE.DoubleSide,
      depthWrite: false
    });
    const mesh = new THREE.Mesh(new THREE.PlaneGeometry(16, 8), mat);
    mesh.renderOrder = 20;
    return mesh;
  }

  function createCrossAttentionBridge(xS, yS, xE, yE) {
    const group = new THREE.Group();

    const curve = new THREE.CubicBezierCurve3(
      new THREE.Vector3(xS, yS, 0),
      new THREE.Vector3(xS + 8, yS, 0),
      new THREE.Vector3(xE - 8, yE, 0),
      new THREE.Vector3(xE, yE, 0)
    );

    const pts = curve.getPoints(40);
    const geo = new THREE.BufferGeometry().setFromPoints(pts);
    const mat = new THREE.LineBasicMaterial({
      color: COLORS.residual,
      linewidth: 3,
      transparent: true,
      depthWrite: false
    });
    const line = new THREE.Line(geo, mat);
    line.renderOrder = 16;
    group.add(line);

    const arrowGeo = new THREE.ConeGeometry(0.8, 2, 8);
    const arrowMat = new THREE.MeshBasicMaterial({
      color: COLORS.residual,
      transparent: true,
      depthWrite: false
    });
    const arrow = new THREE.Mesh(arrowGeo, arrowMat);
    arrow.renderOrder = 16;

    const tangent = curve.getTangent(1).normalize();
    const axis = new THREE.Vector3(0, 1, 0);
    arrow.quaternion.setFromUnitVectors(axis, tangent);
    arrow.position.copy(curve.getPoint(1));
    group.add(arrow);

    const canvas = document.createElement("canvas");
    canvas.width = 256;
    canvas.height = 128;
    const ctx = canvas.getContext("2d");
    ctx.fillStyle = "#" + new THREE.Color(COLORS.residual).getHexString();
    ctx.font = 'bold 44px "Courier New", monospace';
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillText("Residual", 128, 64);

    const tex = new THREE.CanvasTexture(canvas);
    const labelMat = new THREE.MeshBasicMaterial({
      map: tex,
      transparent: true,
      depthWrite: false
    });
    const labelPlane = new THREE.Mesh(new THREE.PlaneGeometry(8, 4), labelMat);
    labelPlane.renderOrder = 17;

    const midPt = curve.getPoint(0.5);
    labelPlane.position.copy(midPt);
    labelPlane.position.y += 2.5;
    group.add(labelPlane);

    scene.add(group);

    volumes.push({
      volMesh: arrow,
      wireMesh: line,
      labelPlane: labelPlane,
      worldPosTarget: group,
      baseOp: 1.0,
      edgeOp: 1.0,
      labelOp: 1.0,
      meshFadeStart: 70,
      meshFadeEnd: 150,
      wireFadeStart: 70,
      wireFadeEnd: 150,
      isCodeOpen: false
    });
  }

  function createEnergyCylinder(group, radius, h, colorHex, type, labelText) {
    const geo = new THREE.CylinderGeometry(radius, radius, h, 32);
    const mat = new THREE.MeshBasicMaterial({
      color: colorHex,
      transparent: true,
      opacity: 0.1,
      depthWrite: false,
      side: THREE.DoubleSide
    });
    const volume = new THREE.Mesh(geo, mat);
    volume.renderOrder = 15;

    const canvas = document.createElement("canvas");
    canvas.width = 512;
    canvas.height = 256;
    const ctx = canvas.getContext("2d");
    ctx.fillStyle = "#000000";
    ctx.font = 'bold 52px "Courier New", monospace';
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";

    const lines = labelText.split("\n");
    const startY = 128 - ((lines.length - 1) * 60) / 2;
    lines.forEach((line, i) => ctx.fillText(line, 256, startY + i * 60));

    const tex = new THREE.CanvasTexture(canvas);
    const labelMat = new THREE.MeshBasicMaterial({
      map: tex,
      transparent: true,
      depthWrite: false,
      side: THREE.DoubleSide
    });

    const labelW = radius * 1.8;
    const labelH = labelW * 0.5;
    const labelPlane = new THREE.Mesh(
      new THREE.PlaneGeometry(labelW, labelH),
      labelMat
    );
    labelPlane.renderOrder = 17;
    labelPlane.position.set(0, h / 2 - labelH / 2 - 0.2, radius + 0.05);

    const subGroup = new THREE.Group();
    subGroup.add(volume);
    subGroup.add(labelPlane);

    const volData = {
      volMesh: volume,
      wireMesh: null,
      labelPlane: labelPlane,
      worldPosTarget: group,
      baseOp: 1.0,
      edgeOp: 0.0,
      labelOp: 1.0,
      meshFadeStart: 70,
      meshFadeEnd: 150,
      wireFadeStart: 45,
      wireFadeEnd: 85,
      isCodeOpen: false
    };

    const codeMesh = createCodePlane(type);
    if (codeMesh) {
      const isRight = group.position.x >= 0;
      const edgeX = isRight ? radius + 0.5 : -radius - 0.5;
      codeMesh.geometry.translate(isRight ? 8 : -8, 0, 0);
      codeMesh.position.set(edgeX, 0, radius + 0.1);
      codeMesh.scale.set(0.001, 1, 1);
      codeMesh.visible = false;
      subGroup.add(codeMesh);
      volData.codeMesh = codeMesh;
    }

    tag(subGroup, type, volData);
    group.add(subGroup);
    volumes.push(volData);
    return subGroup;
  }

  function createEnergyVolume(group, w, h, d, colorHex, type, labelText) {
    const geo = new THREE.BoxGeometry(w, h, d);
    const mat = new THREE.MeshBasicMaterial({
      color: colorHex,
      transparent: true,
      opacity: 0.1,
      depthWrite: false,
      side: THREE.DoubleSide
    });
    const volume = new THREE.Mesh(geo, mat);
    volume.renderOrder = 15;

    const edgeMat = new THREE.LineBasicMaterial({
      color: colorHex,
      transparent: true,
      opacity: 1.0,
      depthWrite: false
    });
    const wire = new THREE.LineSegments(new THREE.EdgesGeometry(geo), edgeMat);
    wire.renderOrder = 16;

    const canvas = document.createElement("canvas");
    canvas.width = 512;
    canvas.height = 256;
    const ctx = canvas.getContext("2d");
    ctx.fillStyle = "#000000";
    ctx.font = 'bold 56px "Courier New", monospace';
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";

    const lines = labelText.split("\n");
    const startY = 128 - ((lines.length - 1) * 60) / 2;
    lines.forEach((line, i) => ctx.fillText(line, 256, startY + i * 60));

    const tex = new THREE.CanvasTexture(canvas);
    const labelMat = new THREE.MeshBasicMaterial({
      map: tex,
      transparent: true,
      depthWrite: false,
      side: THREE.DoubleSide
    });

    const labelW = w * 0.9;
    const labelH = labelW * 0.5;
    const labelPlane = new THREE.Mesh(
      new THREE.PlaneGeometry(labelW, labelH),
      labelMat
    );
    labelPlane.renderOrder = 17;
    labelPlane.position.set(0, h / 2 - labelH / 2 - 0.2, d / 2 + 0.05);

    const subGroup = new THREE.Group();
    subGroup.add(volume);
    subGroup.add(wire);
    subGroup.add(labelPlane);

    const volData = {
      volMesh: volume,
      wireMesh: wire,
      labelPlane: labelPlane,
      worldPosTarget: group,
      baseOp: 1.0,
      edgeOp: 1.0,
      labelOp: 1.0,
      meshFadeStart: 70,
      meshFadeEnd: 150,
      wireFadeStart: 45,
      wireFadeEnd: 85,
      isCodeOpen: false
    };

    const codeMesh = createCodePlane(type);
    if (codeMesh) {
      const isRight = group.position.x >= 0;
      const edgeX = isRight ? w / 2 + 0.5 : -w / 2 - 0.5;
      codeMesh.geometry.translate(isRight ? 8 : -8, 0, 0);
      codeMesh.position.set(edgeX, 0, d / 2 + 0.1);
      codeMesh.scale.set(0.001, 1, 1);
      codeMesh.visible = false;
      subGroup.add(codeMesh);
      volData.codeMesh = codeMesh;
    }

    tag(subGroup, type, volData);
    group.add(subGroup);
    volumes.push(volData);
    return subGroup;
  }

  function createEncapsulationBox(
    x,
    y,
    z,
    w,
    h,
    d,
    colorHex,
    labelText,
    alignLeft
  ) {
    const group = new THREE.Group();
    group.position.set(x, y, z);
    const geo = new THREE.BoxGeometry(w, h, d);
    const mat = new THREE.MeshBasicMaterial({
      color: colorHex,
      transparent: true,
      opacity: 0.1,
      depthWrite: false,
      side: THREE.DoubleSide
    });
    const volume = new THREE.Mesh(geo, mat);

    const edgeMat = new THREE.LineBasicMaterial({
      color: colorHex,
      transparent: true,
      opacity: 1.0,
      depthWrite: false,
      linewidth: 2
    });
    const wire = new THREE.LineSegments(new THREE.EdgesGeometry(geo), edgeMat);

    const canvas = document.createElement("canvas");
    canvas.width = 512;
    canvas.height = 512;
    const ctx = canvas.getContext("2d");
    ctx.fillStyle = "#" + new THREE.Color(colorHex).getHexString();
    ctx.font = 'bold 80px "Courier New", monospace';
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";

    const lines = labelText.split("\n");
    const startY = 256 - ((lines.length - 1) * 90) / 2;
    lines.forEach((line, i) => ctx.fillText(line, 256, startY + i * 90));

    const tex = new THREE.CanvasTexture(canvas);
    const labelMat = new THREE.MeshBasicMaterial({
      map: tex,
      transparent: true,
      depthWrite: false,
      side: THREE.DoubleSide
    });

    const labelW = w * 0.8;
    const labelH = labelW * 1.0;
    const labelPlane = new THREE.Mesh(
      new THREE.PlaneGeometry(labelW, labelH),
      labelMat
    );

    if (alignLeft)
      labelPlane.position.set(-w / 2 - labelW / 2 + 1, 0, d / 2 + 0.1);
    else labelPlane.position.set(w / 2 + labelW / 2 - 1, 0, d / 2 + 0.1);

    group.add(volume);
    group.add(wire);
    group.add(labelPlane);
    scene.add(group);

    volumes.push({
      volMesh: volume,
      wireMesh: wire,
      labelPlane: labelPlane,
      worldPosTarget: group,
      baseOp: 0.02,
      edgeOp: 1.0,
      labelOp: 1.0,
      meshFadeStart: 80,
      meshFadeEnd: 160,
      wireFadeStart: 80,
      wireFadeEnd: 160,
      isCodeOpen: false
    });
  }

  function toggleCode(vol) {
    if (!vol.codeMesh) return;
    if (vol.isCodeOpen) {
      gsap.to(vol.codeMesh.scale, {
        x: 0.001,
        duration: 0.25,
        ease: "power2.in",
        onComplete: () => {
          vol.codeMesh.visible = false;
        }
      });
      vol.isCodeOpen = false;
    } else {
      volumes.forEach((v) => {
        if (v !== vol && v.isCodeOpen) toggleCode(v);
      });
      vol.codeMesh.visible = true;
      vol.codeMesh.scale.x = 0.001;
      gsap.to(vol.codeMesh.scale, { x: 1, duration: 0.3, ease: "back.out(1.2)" });
      vol.isCodeOpen = true;
    }
  }

  function createTokenEmbedding(x, y, z) {
    const group = new THREE.Group();
    group.position.set(x, y, z);
    createEnergyVolume(
      group,
      10,
      8,
      10,
      COLORS.embed,
      "token-embedding",
      "TOKEN\nEMBEDDING"
    );
    const baseRing = new THREE.Mesh(
      new THREE.TorusGeometry(10, 0.08, 16, 64),
      new THREE.MeshBasicMaterial({
        color: 0x4488ff,
        transparent: true,
        opacity: 0.5
      })
    );
    baseRing.position.y = -6;
    baseRing.rotation.x = Math.PI / 2;
    group.add(baseRing);
    const streamGroup = new THREE.Group();
    const funnelColor = new THREE.Color(0x4488ff);
    for (let i = 0; i < 40; i++) {
      const startX = (Math.random() - 0.5) * 20;
      const startZ = (Math.random() - 0.5) * 20;
      const endX = (Math.random() - 0.5) * 4;
      const endZ = (Math.random() - 0.5) * 4;
      const curve = new THREE.CubicBezierCurve3(
        new THREE.Vector3(startX, -6, startZ),
        new THREE.Vector3(startX * 0.5, -4, startZ * 0.5),
        new THREE.Vector3(endX, -3, endZ),
        new THREE.Vector3(endX, -2, endZ)
      );
      const pts = curve.getPoints(20);
      const mat = new THREE.LineBasicMaterial({
        color: funnelColor,
        transparent: true,
        opacity: 0.25,
        blending: THREE.AdditiveBlending
      });
      const line = new THREE.Line(
        new THREE.BufferGeometry().setFromPoints(pts),
        mat
      );
      streamGroup.add(line);
    }
    group.add(streamGroup);
    const matrix = new THREE.Group();
    for (let i = 0; i < 6; i++) {
      for (let j = 0; j < 6; j++) {
        if (Math.random() > 0.2) {
          const mesh = new THREE.Mesh(
            new THREE.BoxGeometry(0.5, 0.5, 0.5),
            new THREE.MeshStandardMaterial({
              color: COLORS.embed,
              emissive: COLORS.embed,
              emissiveIntensity: 2.0 + Math.random() * 2
            })
          );
          mesh.position.set(i * 0.8 - 2.0, -2, j * 0.8 - 2.0);
          matrix.add(mesh);
        }
      }
    }
    group.add(matrix);
    const lookup = new THREE.Mesh(
      new THREE.CylinderGeometry(3.5, 3.5, 0.2, 16),
      new THREE.MeshStandardMaterial({
        color: 0xffffff,
        emissive: COLORS.embed,
        emissiveIntensity: 4.0
      })
    );
    lookup.position.y = 0.5;
    group.add(lookup);
    const webColor = new THREE.Color(COLORS.embed).multiplyScalar(2.0);
    const wireMat = new THREE.LineBasicMaterial({
      color: webColor,
      transparent: true,
      opacity: 0.8,
      depthWrite: false
    });
    const pts = [];
    for (let i = 0; i < 12; i++) {
      pts.push((Math.random() - 0.5) * 4, -2, (Math.random() - 0.5) * 4);
      pts.push((Math.random() - 0.5) * 6, 0.5, (Math.random() - 0.5) * 6);
    }
    const wires = new THREE.LineSegments(
      new THREE.BufferGeometry().setAttribute(
        "position",
        new THREE.Float32BufferAttribute(pts, 3)
      ),
      wireMat
    );
    wires.renderOrder = 10;
    group.add(wires);
    scene.add(group);
  }

  function createPositionalEncoding(x, y, z) {
    const group = new THREE.Group();
    group.position.set(x, y, z);
    createEnergyCylinder(
      group,
      5.5,
      8,
      COLORS.pe,
      "pos-encoding",
      "POS\nENCODING"
    );
    const core = new THREE.Mesh(
      new THREE.CylinderGeometry(0.6, 0.6, 7.5, 32),
      new THREE.MeshStandardMaterial({
        color: 0xffffff,
        emissive: 0xaaccff,
        emissiveIntensity: 2.0,
        transparent: true,
        opacity: 0.3
      })
    );
    group.add(core);
    animatedObjects.push({ mesh: core, type: "pulse", speed: 0.05 });
    const numDims = 64;
    for (let i = 0; i < numDims; i++) {
      const isSine = i % 2 === 0;
      const color = isSine ? COLORS.posSine : COLORS.posCosine;
      const dimPair = Math.floor(i / 2);
      const freq = 20.0 / Math.pow(10, dimPair / (numDims / 2));
      const pts = [];
      const baseRadius = 1.0 + (dimPair / (numDims / 2)) * 4.0;
      const angle = (i / numDims) * Math.PI * 2;
      for (let j = 0; j <= 100; j++) {
        const t = j / 100;
        const h = t * 7.5 - 3.75;
        const phaseOffset = isSine ? 0 : Math.PI / 2;
        const waveOffset = Math.sin(h * freq + phaseOffset) * 0.3;
        pts.push(
          Math.cos(angle) * (baseRadius + waveOffset),
          h,
          Math.sin(angle) * (baseRadius + waveOffset)
        );
      }
      const lineMat = new THREE.LineBasicMaterial({
        color: new THREE.Color(color).multiplyScalar(2.0),
        transparent: true,
        opacity: 0.4,
        depthWrite: false,
        blending: THREE.AdditiveBlending
      });
      const line = new THREE.Line(
        new THREE.BufferGeometry().setAttribute(
          "position",
          new THREE.Float32BufferAttribute(pts, 3)
        ),
        lineMat
      );
      line.renderOrder = 10;
      group.add(line);
      const pulse = new THREE.Mesh(
        new THREE.SphereGeometry(0.06, 8, 8),
        new THREE.MeshBasicMaterial({ color: 0xffffff })
      );
      group.add(pulse);
      animatedObjects.push({
        mesh: pulse,
        type: "pe_pulse",
        curvePts: pts,
        speed: 0.15 + freq * 0.05,
        offset: Math.random()
      });
    }
    for (let i = 0; i < 5; i++) {
      const ring = new THREE.Mesh(
        new THREE.TorusGeometry(5.2, 0.04, 16, 64),
        new THREE.MeshBasicMaterial({
          color: 0xffffff,
          transparent: true,
          opacity: 0.8,
          blending: THREE.AdditiveBlending
        })
      );
      ring.rotation.x = Math.PI / 2;
      group.add(ring);
      animatedObjects.push({ mesh: ring, type: "scan_ring", offset: i / 5 });
    }
    scene.add(group);
  }

  function createAddNorm(x, y, z) {
    const group = new THREE.Group();
    group.position.set(x, y, z);
    createEnergyVolume(
      group,
      6,
      4.5,
      6,
      COLORS.addNorm,
      "add-norm",
      "ADD &\nNORM"
    );
    const chamber = new THREE.Group();
    group.add(chamber);
    chamber.add(
      new THREE.Mesh(
        new THREE.CylinderGeometry(1.5, 1.5, 1.8, 16, 1, true),
        createGlass(COLORS.addNorm, 0.3)
      )
    );
    const capMat = new THREE.MeshStandardMaterial({
      color: 0x444455,
      metalness: 0.8,
      roughness: 0.2
    });
    const top = new THREE.Mesh(
      new THREE.CylinderGeometry(1.6, 1.6, 0.1, 16),
      capMat
    );
    top.position.y = 0.9;
    chamber.add(top);
    const bot = top.clone();
    bot.position.y = -0.9;
    chamber.add(bot);
    const plasma = new THREE.Mesh(
      new THREE.CylinderGeometry(0.6, 0.6, 1.6, 16),
      new THREE.MeshStandardMaterial({
        color: COLORS.addNorm,
        emissive: COLORS.addNorm,
        emissiveIntensity: 2.0,
        transparent: true,
        opacity: 0.8
      })
    );
    chamber.add(plasma);
    animatedObjects.push({ mesh: plasma, type: "pulse", speed: 0.02 });
    const rGeo = new THREE.TorusGeometry(1.2, 0.04, 8, 32);
    const gR = new THREE.Mesh(
      rGeo,
      new THREE.MeshBasicMaterial({ color: 0x00ffcc })
    );
    gR.position.y = 1.1;
    gR.rotation.x = Math.PI / 2;
    group.add(gR);
    const bR = new THREE.Mesh(
      rGeo,
      new THREE.MeshBasicMaterial({ color: 0xaa88ff })
    );
    bR.position.y = 1.3;
    bR.rotation.x = Math.PI / 2;
    group.add(bR);
    scene.add(group);
  }

  function createAttention(x, y, z, isCross = false, isMasked = false) {
    const group = new THREE.Group();
    group.position.set(x, y, z);
    const labelText = isCross
      ? "CROSS\nATTENTION"
      : isMasked
        ? "MASKED\nATTENTION"
        : "SELF\nATTENTION";
    const typeText = isCross
      ? "cross-attention"
      : isMasked
        ? "masked-attention"
        : "self-attention";
    createEnergyVolume(group, 11, 7.5, 11, COLORS.attention, typeText, labelText);
    const baseColor = COLORS.attention;
    const ringGeo = new THREE.TorusGeometry(1.3, 0.06, 12, 48);
    [
      [-3.2, COLORS.qLine],
      [-2.9, COLORS.kLine],
      [-2.6, COLORS.vLine]
    ].forEach((p) => {
      const r = new THREE.Mesh(
        ringGeo,
        new THREE.MeshStandardMaterial({
          color: p[1],
          emissive: p[1],
          emissiveIntensity: 3.0
        })
      );
      r.position.y = p[0];
      r.rotation.x = Math.PI / 2;
      group.add(r);
    });
    if (isCross) {
      const receptor = new THREE.Mesh(
        new THREE.TorusGeometry(1.5, 0.2, 12, 16),
        new THREE.MeshStandardMaterial({
          color: COLORS.residual,
          emissive: COLORS.residual,
          emissiveIntensity: 2.0
        })
      );
      receptor.position.set(-8, 0, 0);
      receptor.rotation.y = Math.PI / 2;
      group.add(receptor);
    }
    const ptsAttn = [];
    for (let h = 0; h < NUM_HEADS; h++) {
      const ha = (h / NUM_HEADS) * Math.PI * 2;
      const hx = 4.2 * Math.cos(ha);
      const hz = 4.2 * Math.sin(ha);
      const head = new THREE.Group();
      head.position.set(hx, 0, hz);
      head.lookAt(0, 2.0, 0);
      const qm = new THREE.Mesh(
        particleGeometries.sphere,
        new THREE.MeshStandardMaterial({
          color: COLORS.qLine,
          emissive: COLORS.qLine,
          emissiveIntensity: 3
        })
      );
      qm.position.set(-0.7, -1.5, 0);
      qm.scale.set(0.6, 0.6, 0.6);
      head.add(qm);
      const km = new THREE.Mesh(
        particleGeometries.sphere,
        new THREE.MeshStandardMaterial({
          color: COLORS.kLine,
          emissive: COLORS.kLine,
          emissiveIntensity: 3
        })
      );
      km.position.set(0.7, -1.5, 0);
      km.scale.set(0.6, 0.6, 0.6);
      head.add(km);
      const vm = new THREE.Mesh(
        particleGeometries.sphere,
        new THREE.MeshStandardMaterial({
          color: COLORS.vLine,
          emissive: COLORS.vLine,
          emissiveIntensity: 3
        })
      );
      vm.position.set(1.5, 0.5, 0);
      vm.scale.set(0.6, 0.6, 0.6);
      head.add(vm);
      const dpNode = new THREE.Mesh(
        new THREE.OctahedronGeometry(0.3, 0),
        new THREE.MeshStandardMaterial({
          color: 0xffffff,
          emissive: 0xffffff,
          emissiveIntensity: 5,
          wireframe: true
        })
      );
      dpNode.position.set(0, -0.5, 0);
      head.add(dpNode);
      animatedObjects.push({ mesh: dpNode, type: "spin", speed: 0.05 });
      const matrix = new THREE.Group();
      matrix.position.set(0, 0.8, 0);
      for (let i = 0; i < 4; i++) {
        for (let j = 0; j < 4; j++) {
          const isMaskedOut = isMasked && j > i;
          const c = isMaskedOut ? 0xff0000 : baseColor;
          const emi = isMaskedOut ? 0.0 : 1.0 + Math.random();
          const op = isMaskedOut ? 0.1 : 0.8;
          const tile = new THREE.Mesh(
            new THREE.BoxGeometry(0.2, 0.2, 0.05),
            new THREE.MeshStandardMaterial({
              color: c,
              emissive: c,
              emissiveIntensity: emi,
              transparent: true,
              opacity: op
            })
          );
          tile.position.set(i * 0.25 - 0.375, j * 0.25 - 0.375, 0);
          matrix.add(tile);
          if (isMaskedOut) {
            const cross = new THREE.LineSegments(
              new THREE.BufferGeometry().setAttribute(
                "position",
                new THREE.Float32BufferAttribute(
                  [
                    -0.06,
                    -0.06,
                    0.03,
                    0.06,
                    0.06,
                    0.03,
                    -0.06,
                    0.06,
                    0.03,
                    0.06,
                    -0.06,
                    0.03
                  ],
                  3
                )
              ),
              new THREE.LineBasicMaterial({ color: 0xff0000 })
            );
            tile.add(cross);
          }
        }
      }
      head.add(matrix);
      const outNode = new THREE.Mesh(
        particleGeometries.sphere,
        new THREE.MeshStandardMaterial({
          color: 0xffffff,
          emissive: baseColor,
          emissiveIntensity: 4
        })
      );
      outNode.position.set(0, 2.2, 0);
      outNode.scale.set(0.8, 0.8, 0.8);
      head.add(outNode);
      head.updateMatrixWorld();
      const localToGroup = (mesh) => {
        const vec = new THREE.Vector3();
        mesh.getWorldPosition(vec);
        group.worldToLocal(vec);
        return vec;
      };
      const qw = localToGroup(qm);
      const kw = localToGroup(km);
      const vw = localToGroup(vm);
      const dw = localToGroup(dpNode);
      const mw = localToGroup(matrix);
      const ow = localToGroup(outNode);
      ptsAttn.push(
        qw.x,
        qw.y,
        qw.z,
        dw.x,
        dw.y,
        dw.z,
        kw.x,
        kw.y,
        kw.z,
        dw.x,
        dw.y,
        dw.z,
        dw.x,
        dw.y,
        dw.z,
        mw.x,
        mw.y,
        mw.z,
        vw.x,
        vw.y,
        vw.z,
        mw.x,
        mw.y,
        mw.z,
        mw.x,
        mw.y,
        mw.z,
        ow.x,
        ow.y,
        ow.z
      );
      ptsAttn.push(0, -3.2, 0, qw.x, qw.y, qw.z);
      if (isCross) {
        ptsAttn.push(-8, 0, 0, kw.x, kw.y, kw.z, -8, 0, 0, vw.x, vw.y, vw.z);
      } else {
        ptsAttn.push(0, -2.9, 0, kw.x, kw.y, kw.z, 0, -2.6, 0, vw.x, vw.y, vw.z);
      }
      ptsAttn.push(ow.x, ow.y, ow.z, 0, 3.2, 0);
      group.add(head);
    }
    const webColor = new THREE.Color(baseColor).multiplyScalar(2.0);
    const qkvMat = new THREE.LineBasicMaterial({
      color: webColor,
      transparent: true,
      opacity: 0.3,
      blending: THREE.AdditiveBlending,
      depthWrite: false
    });
    const qkvWeb = new THREE.LineSegments(
      new THREE.BufferGeometry().setAttribute(
        "position",
        new THREE.Float32BufferAttribute(ptsAttn, 3)
      ),
      qkvMat
    );
    qkvWeb.renderOrder = 10;
    group.add(qkvWeb);
    const woRing = new THREE.Mesh(
      new THREE.TorusGeometry(1.5, 0.08, 16, 48),
      new THREE.MeshStandardMaterial({
        color: baseColor,
        emissive: baseColor,
        emissiveIntensity: 2.0
      })
    );
    woRing.position.y = 3.2;
    woRing.rotation.x = Math.PI / 2;
    group.add(woRing);
    scene.add(group);
  }

  function createFeedForward(x, y, z) {
    const group = new THREE.Group();
    group.position.set(x, y, z);
    createEnergyVolume(group, 15, 6.5, 15, COLORS.ffn, "ffn", "FEED\nFORWARD");
    const color = COLORS.ffn;
    const numIn = 16;
    const numHid = 64;
    const rIn = 1.2;
    const rHid = 4.5;
    const inNodes = [];
    for (let i = 0; i < numIn; i++) {
      let a = (i / numIn) * Math.PI * 2;
      let pos = new THREE.Vector3(Math.cos(a) * rIn, -2.5, Math.sin(a) * rIn);
      inNodes.push(pos);
      const m = new THREE.Mesh(
        new THREE.SphereGeometry(0.1, 8, 8),
        new THREE.MeshStandardMaterial({
          color: 0xffffff,
          emissive: 0xffffff,
          emissiveIntensity: 2.0
        })
      );
      m.position.copy(pos);
      group.add(m);
    }
    const outNodes = [];
    for (let i = 0; i < numIn; i++) {
      let a = (i / numIn) * Math.PI * 2;
      let pos = new THREE.Vector3(Math.cos(a) * rIn, 2.5, Math.sin(a) * rIn);
      outNodes.push(pos);
      const m = new THREE.Mesh(
        new THREE.SphereGeometry(0.1, 8, 8),
        new THREE.MeshStandardMaterial({
          color: 0xffffff,
          emissive: 0xffffff,
          emissiveIntensity: 2.0
        })
      );
      m.position.copy(pos);
      group.add(m);
    }
    const hiddenNodes = [];
    for (let i = 0; i < numHid; i++) {
      let a = (i / numHid) * Math.PI * 2;
      let pos = new THREE.Vector3(Math.cos(a) * rHid, 0, Math.sin(a) * rHid);
      hiddenNodes.push(pos);
      const isActive = Math.random() > 0.5;
      const m = new THREE.Mesh(
        new THREE.IcosahedronGeometry(0.12, 0),
        new THREE.MeshStandardMaterial({
          color: isActive ? color : 0x222222,
          emissive: isActive ? color : 0x000000,
          emissiveIntensity: isActive ? 3.0 : 0,
          transparent: true,
          opacity: isActive ? 0.9 : 0.2
        })
      );
      m.position.copy(pos);
      group.add(m);
    }
    const ptsFFN = [];
    for (let i = 0; i < numIn; i++) {
      for (let j = 0; j < numHid; j++) {
        ptsFFN.push(
          inNodes[i].x,
          inNodes[i].y,
          inNodes[i].z,
          hiddenNodes[j].x,
          hiddenNodes[j].y,
          hiddenNodes[j].z
        );
      }
    }
    for (let j = 0; j < numHid; j++) {
      for (let i = 0; i < numIn; i++) {
        ptsFFN.push(
          hiddenNodes[j].x,
          hiddenNodes[j].y,
          hiddenNodes[j].z,
          outNodes[i].x,
          outNodes[i].y,
          outNodes[i].z
        );
      }
    }
    const webColor = new THREE.Color(color).multiplyScalar(1.5);
    const webMat = new THREE.LineBasicMaterial({
      color: webColor,
      transparent: true,
      opacity: 0.05,
      blending: THREE.AdditiveBlending,
      depthWrite: false
    });
    const webFFN = new THREE.LineSegments(
      new THREE.BufferGeometry().setAttribute(
        "position",
        new THREE.Float32BufferAttribute(ptsFFN, 3)
      ),
      webMat
    );
    webFFN.renderOrder = 10;
    group.add(webFFN);
    scene.add(group);
  }

  function createOutputLayer(x, y, z) {
    const group = new THREE.Group();
    group.position.set(x, y, z);
    createEnergyVolume(
      group,
      20,
      13,
      20,
      COLORS.softmax,
      "softmax",
      "LINEAR &\nSOFTMAX"
    );
    const proj = new THREE.Mesh(
      new THREE.CylinderGeometry(8.5, 1.2, 6, 24, 1, true),
      new THREE.MeshBasicMaterial({
        color: 0x44aaff,
        wireframe: true,
        transparent: true,
        opacity: 0.1
      })
    );
    proj.position.y = 3;
    group.add(proj);
    const plate = new THREE.Mesh(
      new THREE.CylinderGeometry(9, 9, 0.2, 48),
      new THREE.MeshStandardMaterial({
        color: 0x0a1020,
        emissive: 0x111122,
        emissiveIntensity: 0.5
      })
    );
    plate.position.y = 6;
    group.add(plate);
    const bars = [];
    for (let i = 0; i < 120; i++) {
      const angle = (i / 120) * Math.PI * 2;
      const bar = new THREE.Mesh(
        new THREE.BoxGeometry(0.15, 1, 0.15),
        new THREE.MeshStandardMaterial({
          color: COLORS.softmax,
          emissive: COLORS.softmax,
          emissiveIntensity: 0.5
        })
      );
      bar.geometry.translate(0, 0.5, 0);
      bar.position.set(Math.cos(angle) * 8.5, 6.1, Math.sin(angle) * 8.5);
      bar.lookAt(0, 6.1, 0);
      group.add(bar);
      bars.push(bar);
    }
    scene.add(group);
    function loop() {
      const w = Math.floor(Math.random() * 120);
      window.activeSoftmaxAngle = (w / 120) * Math.PI * 2;
      bars.forEach((b, i) => {
        let diff = Math.abs(i - w);
        if (diff > 60) diff = 120 - diff;
        const p = Math.exp(-diff * 0.6);
        gsap.to(b.scale, {
          y: 0.1 + p * 18,
          duration: 0.5,
          ease: "back.out(1.5)"
        });
        let baseColor = new THREE.Color(COLORS.softmax);
        let activeColor = new THREE.Color(0xffaa00);
        let targetColor = baseColor.lerp(activeColor, p);
        gsap.to(b.material.color, {
          r: targetColor.r,
          g: targetColor.g,
          b: targetColor.b,
          duration: 0.5
        });
        gsap.to(b.material.emissive, {
          r: targetColor.r,
          g: targetColor.g,
          b: targetColor.b,
          duration: 0.5
        });
        gsap.to(b.material, { emissiveIntensity: 0.5 + p * 10.0, duration: 0.5 });
      });
      setTimeout(loop, 900);
    }
    loop();
  }

  function buildArchitecture() {
    [
      [-12, false],
      [12, true]
    ].forEach((s) => {
      createTokenEmbedding(s[0], -12, 0);
      createPositionalEncoding(s[0], -2, 0);
      for (let i = 0; i < NUM_BLOCKS; i++) {
        let by = 10 + i * 38;
        if (!s[1]) {
          createAttention(-12, by, 0, false, false);
          createAddNorm(-12, by + 6, 0);
          createFeedForward(-12, by + 16, 0);
          createAddNorm(-12, by + 23, 0);
        } else {
          createAttention(12, by, 0, false, true);
          createAddNorm(12, by + 6, 0);
          createAttention(12, by + 14, 0, true, false);
          createAddNorm(12, by + 20, 0);
          createFeedForward(12, by + 30, 0);
          createAddNorm(12, by + 37, 0);
          paths.push({
            curve: new THREE.CubicBezierCurve3(
              new THREE.Vector3(-12, 35, 0),
              new THREE.Vector3(-4, 35, 0),
              new THREE.Vector3(-4, 24, 0),
              new THREE.Vector3(4, 24, 0)
            ),
            targetY: 24
          });
        }
      }
    });
    createOutputLayer(12, 58, 0);
    createEncapsulationBox(
      -12,
      21,
      0,
      18,
      32,
      18,
      COLORS.encoderBox,
      "THE\nENCODER",
      true
    );
    createEncapsulationBox(
      12,
      28,
      0,
      18,
      46,
      18,
      COLORS.decoderBox,
      "THE\nDECODER",
      false
    );
    createCrossAttentionBridge(-12, 35, 4, 24);
  }

  function createDataFlow() {
    const mat = new THREE.MeshStandardMaterial({
      emissiveIntensity: 3.5,
      metalness: 0.2
    });
    for (let i = 0; i < 450; i++) {
      const group = new THREE.Group();
      const m = new THREE.Mesh(particleGeometries.cube, mat.clone());
      const spriteMat = new THREE.SpriteMaterial({
        map: binaryTex[0],
        transparent: true,
        blending: THREE.AdditiveBlending,
        depthWrite: false
      });
      const sprite = new THREE.Sprite(spriteMat);
      sprite.visible = false;
      group.add(m);
      group.add(sprite);
      scene.add(group);
      particles.push({
        group: group,
        mesh: m,
        sprite: sprite,
        type: Math.random() < 0.5 ? "encoder" : "decoder",
        progress: Math.random(),
        speed: 0.002 + Math.random() * 0.002,
        baseAngle: Math.random() * Math.PI * 2,
        isResidual: Math.random() < 0.4,
        qkvRole: ["Q", "K", "V"][Math.floor(Math.random() * 3)],
        headIndex: Math.floor(Math.random() * NUM_HEADS),
        ffnRadius: 4.5,
        ffnAngle: Math.random() * Math.PI * 2,
        reluActive: Math.random() > 0.5,
        currentZone: "",
        targetScale: new THREE.Vector3(1, 1, 1),
        prevPos: new THREE.Vector3(),
        gridX: (Math.random() - 0.5) * 8,
        gridZ: (Math.random() - 0.5) * 8
      });
    }
  }

  function setupInteraction() {
    const can = renderer.domElement;
    can.addEventListener("pointerdown", (e) => {
      activePointers++;
      pointerDownPos.set(e.clientX, e.clientY);
      pointerCurrentPos.set(e.clientX, e.clientY);
      isPointerDown = true;
      isHolding = true;
      pointerDownTimestamp = performance.now();
    });
    can.addEventListener("pointermove", (e) => {
      pointerCurrentPos.set(e.clientX, e.clientY);
      if (isHolding && pointerDownPos.distanceTo(pointerCurrentPos) > 15)
        isHolding = false;
    });
    can.addEventListener("pointerup", (e) => {
      activePointers = Math.max(0, activePointers - 1);
      isPointerDown = false;
      isHolding = false;
      if (
        pointerDownPos.distanceTo(new THREE.Vector2(e.clientX, e.clientY)) < 15
      ) {
        const now = performance.now();
        if (now - lastTapTime < 350) handleDoubleTap(e);
        else handleRaycast(e);
        lastTapTime = now;
      }
    });
    can.addEventListener("pointercancel", (e) => {
      activePointers = Math.max(0, activePointers - 1);
      isPointerDown = false;
      isHolding = false;
    });
    can.addEventListener("pointerleave", (e) => {
      activePointers = Math.max(0, activePointers - 1);
      isPointerDown = false;
      isHolding = false;
    });
  }

  function handleDoubleTap(e) {
    controls.autoRotate = !controls.autoRotate;
    mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
    raycaster.setFromCamera(mouse, camera);
    const hit = raycaster
      .intersectObjects(scene.children, true)
      .find((h) => h.object.userData && h.object.userData.blockType);
    if (hit) {
      const tP = new THREE.Vector3().copy(
        hit.object.parent ? hit.object.parent.position : hit.object.position
      );
      gsap.to(controls.target, { x: tP.x, y: tP.y, z: tP.z, duration: 1 });
      gsap.to(camera.position, { y: tP.y + 10, duration: 1 });
    }
  }

  function handleRaycast(e) {
    mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
    raycaster.setFromCamera(mouse, camera);
    const hit = raycaster
      .intersectObjects(scene.children, true)
      .find((h) => h.object.userData && h.object.userData.volumeRef);
    if (hit) {
      const vol = hit.object.userData.volumeRef;
      if (vol.codeMesh) toggleCode(vol);
    }
  }

  function routeAttention(x, by, dy, p, role, isC, isM) {
    let t = (dy + 3.2) / 6.4,
      ha = (p.headIndex / NUM_HEADS) * Math.PI * 2,
      hx = 4.2 * Math.cos(ha),
      hz = 4.2 * Math.sin(ha);
    let px = x,
      py = by + dy,
      pz = 0,
      color = COLORS.particle;
    if (isM && isC) {
      if (t < 0.4) {
        let lt = t / 0.4;
        px = x - 8 + (8 + hx) * lt;
        pz = (role === "K" ? hz + 1.8 : hz) * lt;
        color = role === "K" ? COLORS.kLine : COLORS.vLine;
      } else if (t < 0.6) {
        px = x + hx;
        pz = role === "K" ? hz + 1.8 : hz;
        color = 0xffffff;
      } else {
        let lt = (t - 0.6) / 0.4;
        px = (x + hx) * (1 - lt) + x * lt;
        pz = hz * (1 - lt);
        color = COLORS.attention;
      }
    } else {
      if (t < 0.15) {
        color =
          role === "Q"
            ? COLORS.qLine
            : role === "K"
            ? COLORS.kLine
            : COLORS.vLine;
      } else if (t < 0.3) {
        let lt = (t - 0.15) / 0.15;
        px = x + hx * lt;
        pz = role === "V" ? hz * lt : hz * lt + 1.8 * lt;
        color =
          role === "Q"
            ? COLORS.qLine
            : role === "K"
            ? COLORS.kLine
            : COLORS.vLine;
      } else if (t < 0.5) {
        px = x + hx;
        pz = role === "V" ? hz : hz + 1.8;
        color = role === "V" ? COLORS.vLine : 0xffffff;
      } else if (t < 0.8) {
        px = x + hx;
        pz = hz;
        color = COLORS.attention;
      } else {
        let lt = (t - 0.8) / 0.2;
        px = (x + hx) * (1 - lt) + x * lt;
        pz = hz * (1 - lt);
        color = COLORS.attention;
      }
    }
    return { px, py, pz, color, zone: "attention", scale: 1 };
  }

  function routeFFN(x, by, dy, p, time) {
    let t = (dy + 2.5) / 5;
    let numIn = 16,
      numHid = 64;
    let a_in =
      (Math.floor((p.baseAngle / (Math.PI * 2)) * numIn) / numIn) * Math.PI * 2;
    let a_hid =
      (Math.floor((p.ffnAngle / (Math.PI * 2)) * numHid) / numHid) * Math.PI * 2;
    let x_in = x + Math.cos(a_in) * 1.2;
    let z_in = Math.sin(a_in) * 1.2;
    let x_hid = x + Math.cos(a_hid) * 4.5;
    let z_hid = Math.sin(a_hid) * 4.5;
    let px, pz;
    if (t < 0.5) {
      let lt = t / 0.5;
      px = x_in + (x_hid - x_in) * lt;
      pz = z_in + (z_hid - z_in) * lt;
    } else {
      let lt = (t - 0.5) / 0.5;
      px = x_hid + (x_in - x_hid) * lt;
      pz = z_hid + (z_in - z_hid) * lt;
    }
    let py = by + dy;
    let scale = !p.reluActive ? 0.3 : t < 0.5 ? 1 + t * 2 : 2 - (t - 0.5) * 2;
    return {
      px,
      py,
      pz,
      color: !p.reluActive ? 0x111111 : COLORS.ffn,
      zone: "ffn",
      scale
    };
  }

  function routeStack(x, y, isDec, p, time, isMem) {
    if (y < -14) {
      let t = Math.max(0, Math.min(1, (y + 18) / 4));
      let spread = 3.0 * (1 - t) + 0.5 * t;
      return {
        px: x + p.gridX * spread,
        py: y,
        pz: p.gridZ * spread,
        color: 0x88ccff,
        zone: "raw_text",
        scale: 1
      };
    }
    if (y >= -14 && y < -11.5) {
      return {
        px: x + p.gridX * 0.5,
        py: y,
        pz: p.gridZ * 0.5,
        color: COLORS.embed,
        zone: "token_embed",
        scale: 1
      };
    }
    if (y >= -11.5 && y < -6)
      return {
        px: x,
        py: y,
        pz: 0,
        color: COLORS.particle,
        zone: "trunk",
        scale: 1
      };
    if (y >= -6 && y < 2) {
      return {
        px: x + Math.cos(y * 2 + p.baseAngle + time) * 1.5,
        py: y,
        pz: Math.sin(y * 2 + p.baseAngle + time) * 1.5,
        color: Math.sin(y * 4) > 0 ? COLORS.posSine : COLORS.posCosine,
        zone: "pos_encoding",
        scale: 1
      };
    }
    if (y >= 2 && y < 6.8)
      return {
        px: x,
        py: y,
        pz: 0,
        color: COLORS.particle,
        zone: "trunk",
        scale: 1
      };
    if (isDec && y > 58) {
      let dy = y - 58;
      let lt = Math.min(dy / 6, 1);
      let r = 1.2 + (8.5 - 1.2) * Math.pow(lt, 3);
      let startA = p.baseAngle + time * 6.0;
      let targetA = window.activeSoftmaxAngle || 0;
      let diff = targetA - (startA % (Math.PI * 2));
      if (diff > Math.PI) diff -= Math.PI * 2;
      if (diff < -Math.PI) diff += Math.PI * 2;
      let currentA = startA + diff * Math.pow(lt, 5);
      currentA +=
        Math.sin(time * 15 + p.baseAngle * 20) * 0.5 * (1 - Math.pow(lt, 4));
      let rCol = Math.floor(0x00 + 0xff * lt);
      let gCol = Math.floor(0xff - 0x55 * lt);
      let bCol = Math.floor(0xff - 0xff * lt);
      return {
        px: x + Math.cos(currentA) * r,
        py: y,
        pz: Math.sin(currentA) * r,
        color: (rCol << 16) | (gCol << 8) | bCol,
        zone: "softmax",
        scale: 1 + lt
      };
    }
    for (let i = 0; i < NUM_BLOCKS; i++) {
      let by = 10 + i * 38;
      if (p.isResidual && !isMem) {
        const getBezier = (p0, p1, p2, t) => {
          const u = 1 - t;
          return new THREE.Vector3()
            .addScaledVector(p0, u * u)
            .addScaledVector(p1, 2 * u * t)
            .addScaledVector(p2, t * t);
        };
        if (y >= by - 3.2 && y <= by + 5.2) {
          let pt = getBezier(
            new THREE.Vector3(x, by - 3.2, 0),
            new THREE.Vector3(x + (isDec ? 5 : -5), by + 1.0, 0),
            new THREE.Vector3(x, by + 5.2, 0),
            (y - by + 3.2) / 8.4
          );
          return {
            px: pt.x,
            py: y,
            pz: pt.z,
            color: COLORS.residual,
            zone: "residual",
            scale: 1
          };
        }
        if (isDec && y >= by + 6.8 && y <= by + 19.2) {
          let pt = getBezier(
            new THREE.Vector3(x, by + 6.8, 0),
            new THREE.Vector3(x + 5, by + 13, 0),
            new THREE.Vector3(x, by + 19.2, 0),
            (y - by - 6.8) / 12.4
          );
          return {
            px: pt.x,
            py: y,
            pz: pt.z,
            color: COLORS.residual,
            zone: "residual",
            scale: 1
          };
        }
        let fS = isDec ? by + 20.8 : by + 6.8,
          fE = isDec ? by + 36.2 : by + 22.2,
          fO = isDec ? -5 : 5;
        if (y >= fS && y <= fE) {
          let pt = getBezier(
            new THREE.Vector3(x, fS, 0),
            new THREE.Vector3(x + fO, (fS + fE) / 2, 0),
            new THREE.Vector3(x, fE, 0),
            (y - fS) / (fE - fS)
          );
          return {
            px: pt.x,
            py: y,
            pz: pt.z,
            color: COLORS.residual,
            zone: "residual",
            scale: 1
          };
        }
      }
      if (y >= by - 3.2 && y <= by + 3.2)
        return routeAttention(
          x,
          by,
          y - by,
          p,
          isDec ? "Q" : p.qkvRole,
          false,
          false
        );
      if (y > by + 5.2 && y < by + 6.8)
        return {
          px: x,
          py: y,
          pz: 0,
          color: COLORS.addNorm,
          zone: "norm",
          scale: 0.6
        };
      if (!isDec) {
        if (y >= by + 13.5 && y <= by + 18.5)
          return routeFFN(x, by + 16, y - by - 16, p, time);
        if (y >= by + 22.2 && y < by + 23.8)
          return {
            px: x,
            py: y,
            pz: 0,
            color: COLORS.addNorm,
            zone: "norm",
            scale: 0.6
          };
      } else {
        if (y >= by + 11.5 && y <= by + 16.5)
          return routeAttention(
            x,
            by + 14,
            y - by - 14,
            p,
            isMem ? p.qkvRole : "Q",
            true,
            isMem
          );
        if (y >= by + 19.2 && y < by + 20.8)
          return {
            px: x,
            py: y,
            pz: 0,
            color: COLORS.addNorm,
            zone: "norm",
            scale: 0.6
          };
        if (y >= by + 27.5 && y <= by + 32.5)
          return routeFFN(x, by + 30, y - by - 30, p, time);
        if (y >= by + 36.2 && y < by + 37.8)
          return {
            px: x,
            py: y,
            pz: 0,
            color: COLORS.addNorm,
            zone: "norm",
            scale: 0.6
          };
      }
    }
    return {
      px: x,
      py: y,
      pz: 0,
      color: COLORS.particle,
      zone: "trunk",
      scale: 1
    };
  }

  function animate() {
    requestAnimationFrame(animate);
    const t = performance.now() * 0.001;

    if (
      isHolding &&
      activePointers === 1 &&
      performance.now() - pointerDownTimestamp > 1000
    ) {
      camera.position.y += 0.12;
      controls.target.y += 0.12;
    }

    controls.update();
    const camPos = camera.position;
    volumes.forEach((vol) => {
      const worldPos = new THREE.Vector3();
      vol.worldPosTarget.getWorldPosition(worldPos);
      const dist = camPos.distanceTo(worldPos);
      let meshFactor = Math.max(
        0,
        Math.min(
          1,
          (dist - vol.meshFadeStart) / (vol.meshFadeEnd - vol.meshFadeStart)
        )
      );
      let wireFactor = Math.max(
        0,
        Math.min(
          1,
          (dist - vol.wireFadeStart) / (vol.wireFadeEnd - vol.wireFadeStart)
        )
      );
      if (vol.volMesh && vol.volMesh.material)
        vol.volMesh.material.opacity = vol.baseOp * meshFactor;
      if (vol.wireMesh && vol.wireMesh.material)
        vol.wireMesh.material.opacity = vol.edgeOp * wireFactor;
      if (vol.labelPlane && vol.labelPlane.material)
        vol.labelPlane.material.opacity = vol.labelOp * meshFactor;
    });
    animatedObjects.forEach((o) => {
      if (o.type === "spin") o.mesh.rotation.y += o.speed;
      if (o.type === "pulse")
        o.mesh.scale.x = o.mesh.scale.z = 1 + Math.sin(t * 3) * 0.2;
      if (o.type === "wave")
        o.mesh.scale.x = o.mesh.scale.z =
          1 + Math.sin(t * o.freq + o.offset) * 0.15;
      if (o.type === "pe_pulse") {
        let t_curve = (t * o.speed + o.offset) % 1.0;
        let ptIdx = Math.floor(t_curve * (o.curvePts.length / 3 - 1)) * 3;
        if (o.curvePts[ptIdx] !== undefined)
          o.mesh.position.set(
            o.curvePts[ptIdx],
            o.curvePts[ptIdx + 1],
            o.curvePts[ptIdx + 2]
          );
      }
      if (o.type === "scan_ring") {
        let t_scan = (t * 0.2 + o.offset) % 1.0;
        o.mesh.position.y = t_scan * 7.5 - 3.75;
        o.mesh.material.opacity = Math.sin(t_scan * Math.PI) * 0.8;
      }
    });
    particles.forEach((p) => {
      p.progress += p.type === "cross" ? p.speed * 0.5 : p.speed;
      if (p.progress > 1) {
        p.progress = 0;
        p.gridX = (Math.random() - 0.5) * 8;
        p.gridZ = (Math.random() - 0.5) * 8;
        if (Math.random() < 0.15 && paths.length > 0) {
          p.type = "cross";
          p.qkvRole = Math.random() > 0.5 ? "K" : "V";
        } else {
          p.type = Math.random() < 0.575 ? "decoder" : "encoder";
          p.qkvRole = ["Q", "K", "V"][Math.floor(Math.random() * 3)];
        }
        p.reluActive = Math.random() > 0.5;
      }
      let s;
      if (p.type === "cross") {
        const path = paths[Math.floor(p.baseAngle * 10) % paths.length];
        if (p.progress < 0.3)
          s = routeStack(
            ENCODER_X,
            -18 + (p.progress / 0.3) * 53,
            false,
            p,
            t,
            false
          );
        else if (p.progress < 0.6) {
          let pt = path.curve.getPointAt((p.progress - 0.3) / 0.3);
          s = {
            px: pt.x,
            py: pt.y,
            pz: pt.z,
            color: 0xffffff,
            zone: "cross_bridge",
            scale: 1
          };
        } else
          s = routeStack(
            DECODER_X,
            path.targetY -
            3.2 +
            ((p.progress - 0.6) / 0.4) * (64 - (path.targetY - 3.2)),
            true,
            p,
            t,
            true
          );
      } else
        s = routeStack(
          p.type === "decoder" ? DECODER_X : ENCODER_X,
          -18 + p.progress * (p.type === "decoder" ? 82 : 53),
          p.type === "decoder",
          p,
          t,
          false
        );
      p.group.position.set(s.px, s.py, s.pz);
      if (s.zone !== p.currentZone) {
        p.currentZone = s.zone;
        if (
          s.zone === "trunk" ||
          s.zone === "residual" ||
          s.zone === "cross_bridge"
        ) {
          p.mesh.visible = false;
          p.sprite.visible = true;
          p.sprite.material.map =
            binaryTex[Math.floor(Math.random() * binaryTex.length)];
        } else if (s.zone === "raw_text") {
          p.mesh.visible = false;
          p.sprite.visible = true;
          p.sprite.material.map =
            asciiTex[Math.floor(Math.random() * asciiTex.length)];
        } else if (s.zone === "token_embed") {
          p.mesh.visible = false;
          p.sprite.visible = true;
          p.sprite.material.map = idTex[Math.floor(Math.random() * idTex.length)];
        } else if (s.zone === "softmax") {
          p.mesh.visible = false;
          p.sprite.visible = true;
          p.sprite.material.map =
            asciiTex[Math.floor(Math.random() * asciiTex.length)];
        } else {
          p.sprite.visible = false;
          p.mesh.visible = true;
          let geo = particleGeometries.sphere;
          if (s.zone === "pos_encoding") geo = particleGeometries.torusKnot;
          else if (s.zone === "attention") geo = particleGeometries.beam;
          else if (s.zone === "ffn" || s.zone === "softmax")
            geo = particleGeometries.dataDart;
          p.mesh.geometry = geo;
        }
      }
      if (p.sprite.visible) {
        p.sprite.material.color.setHex(s.color).multiplyScalar(2.5);
        p.targetScale.set(s.scale * 1.5, s.scale * 1.5, s.scale * 1.5);
        p.sprite.scale.lerp(p.targetScale, 0.1);
      } else {
        p.mesh.material.color.setHex(s.color);
        p.mesh.material.emissive.setHex(s.color);
        p.mesh.material.emissiveIntensity = 2.5;
        if (s.zone === "softmax") {
          p.targetScale.set(s.scale * 0.5, s.scale * 0.5, s.scale * 3.0);
        } else {
          p.targetScale.set(s.scale, s.scale, s.scale);
        }
        p.mesh.scale.lerp(p.targetScale, 0.1);
        if (s.zone === "attention" || s.zone === "ffn" || s.zone === "softmax") {
          const v = new THREE.Vector3().subVectors(p.group.position, p.prevPos);
          if (v.lengthSq() > 0.0001)
            p.mesh.lookAt(p.group.position.clone().add(v));
          if (s.zone === "ffn") p.mesh.rotation.z += 0.3;
        } else if (s.zone === "pos_encoding") {
          p.mesh.rotation.x += 0.1;
          p.mesh.rotation.y += 0.15;
          p.mesh.rotation.z += 0.05;
        }
      }
      p.prevPos.copy(p.group.position);
    });
    composer.render();
  }

</script>
</body>
</html>

效果如下

体验

相关推荐
kishu_iOS&AI4 小时前
NLP —— Transformer底层源码剖析(编码器部分)
人工智能·自然语言处理·transformer
m0_634666734 小时前
MeMo:当记忆本身变成一个模型
人工智能·深度学习·ai·ai编程
代码地平线4 小时前
⭐️C++入门基础精讲(一):从发展历史到第一个程序
大数据·c++·后端·深度学习
Hali_Botebie4 小时前
【量化】I-BERT: Integer-only BERT Quantization
人工智能·深度学习·bert
大模型最新论文速读4 小时前
利用异步编程的 future 思想,让 LLM Agent 快 1.44 倍
人工智能·深度学习·算法·机器学习·自然语言处理
AI人工智能+5 小时前
机动车登记证识别技术通过计算机视觉与深度学习实现证件信息自动化提取,显著提升车辆管理效率
深度学习·计算机视觉·自然语言处理·ocr·机动车登记证识别
郑寿昌5 小时前
B200GPU上SubQ模型7.2倍加速秘诀
人工智能·深度学习
盼小辉丶5 小时前
PyTorch强化学习实战(9)——深度Q学习
pytorch·深度学习·强化学习
Omics Pro6 小时前
全流程可重复!R语言脂质组学:原始数据→功能解析
开发语言·人工智能·深度学习·语言模型·r语言·excel·知识图谱