AIGC标识 同一句话,三只国产机械蝴蝶

我把同一句话喂给三个国产大模型,让它们各造一只「机械蝴蝶」——结果差距比我想象的大多了

Prompt 只有一句:

"使用 Three.js 从零编写,渲染出极其复杂精美的「机械蝴蝶」。"

没有设计稿,没有参考图,没有第二句话。然后,三位选手——智谱 GLM-5.3、阿里 Qwen3.8-Max、小米 MiMo-V2.6-Pro——交出了三只完全不同的蝴蝶。

打开页面的那一刻,我愣住了:这不是代码,这是三种人格。


一、GLM-5.3:艺术家,偏执的细节狂魔

它交付的是一只「机械凤蝶 · Machina papilio」,黄铜蒸汽朋克风,一眼钟表匠气质。

  • 翅膀是真·凤蝶翅脉:41 式翅脉全部用贝塞尔曲线数据化定义,再用管状几何逐根"锻造"出来,接缝处还有铆钉螺母;
  • 齿轮是逐齿建模的真实渐开线轮廓,带辐条、带轴孔,而不是拿个圆柱糊弄;
  • 翅膀薄膜用 Canvas 程序化绘制纹理,叠了一层物理虹彩材质(iridescence),转动角度真的会流光;
  • 最狠的是:three.js 的轨道控制器和辉光后期它全都没用官方库,自己手写了 HDR 渲染管线 + 高斯模糊 Bloom + ACES 色调映射,着色器一行行敲出来;
  • 甚至翅膀是顶点级柔性形变——扑翼时翅尖有延迟挥摆,不是整块刚体在那硬甩。

更离谱的是代码里留着一行注释:「★ 修复:横脉条目没有 p0,必须最先分流处理(原先放在后面导致崩溃)」。它自己 debug 了自己。

一句话总结:模型里的手工耿,活儿必较真。

以下为GLM-5.3生成的机械蝴蝶代码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>机械凤蝶 · Machina papilio</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href=" https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital ,wght@0,500;0,600;1,500&family=Noto+Serif+SC:wght@400;600&display=swap" rel="stylesheet">
<style>
  :root{ --ink:#cdb17d; --ink-dim:rgba(205,177,125,.55); --line:rgba(205,177,125,.28); }
  *{ margin:0; padding:0; box-sizing:border-box; }
  html,body{ width:100%; height:100%; overflow:hidden; background:#0b0a08;
    font-family:'Noto Serif SC','Cormorant Garamond',serif; color:var(--ink); }
  canvas{ display:block; touch-action:none; }

  .vignette{ position:fixed; inset:0; pointer-events:none; z-index:5;
    background:radial-gradient(ellipse at 50% 46%, transparent 52%, rgba(0,0,0,.62) 100%); }

  #brand{ position:fixed; top:34px; left:38px; z-index:10; max-width:340px;
    border:1px solid var(--line); padding:20px 24px 18px;
    opacity:0; transform:translateY(10px); transition:opacity 1.4s ease .4s, transform 1.4s ease .4s; }
  body.ready #brand{ opacity:1; transform:none; }
  #brand .tag{ font-size:10px; letter-spacing:.42em; color:var(--ink-dim); margin-bottom:12px; }
  #brand h1{ font-family:'Cormorant Garamond','Noto Serif SC',serif; font-weight:600;
    font-size:40px; line-height:1; letter-spacing:.18em; color:#e6cf9a; }
  #brand .latin{ font-family:'Cormorant Garamond',serif; font-style:italic;
    font-size:15px; margin-top:10px; color:var(--ink-dim); letter-spacing:.06em; }
  #brand .note{ font-size:11px; line-height:1.9; margin-top:14px; padding-top:12px;
    border-top:1px solid var(--line); color:var(--ink-dim); letter-spacing:.12em; }

  #readout{ position:fixed; left:38px; bottom:34px; z-index:10; font-size:11px;
    letter-spacing:.2em; color:var(--ink-dim);
    opacity:0; transition:opacity 1.2s ease 1s; }
  body.ready #readout{ opacity:1; }
  #readout .cap{ font-size:9px; letter-spacing:.4em; margin-bottom:10px; color:rgba(205,177,125,.35); }
  #readout .row{ display:flex; align-items:baseline; gap:14px; margin-top:6px; }
  #readout b{ font-family:'Cormorant Garamond',serif; font-weight:600; font-size:20px;
    color:#e6cf9a; min-width:64px; letter-spacing:.05em; }

  #ctrl{ position:fixed; right:38px; bottom:34px; z-index:10; display:flex;
    align-items:center; gap:10px; flex-wrap:wrap; justify-content:flex-end;
    opacity:0; transition:opacity 1.2s ease 1.2s; }
  body.ready #ctrl{ opacity:1; }
  #ctrl button{ font-family:inherit; font-size:11px; letter-spacing:.28em;
    color:var(--ink); background:rgba(20,16,10,.35); border:1px solid var(--line);
    padding:9px 16px 9px 20px; cursor:pointer; transition:all .25s ease; backdrop-filter:blur(4px); }
  #ctrl button:hover{ border-color:var(--ink); background:rgba(60,46,24,.4); }
  #ctrl button.on{ background:var(--ink); color:#17120a; border-color:var(--ink); }
  .slider{ display:flex; align-items:center; gap:12px; margin-left:8px;
    font-size:10px; letter-spacing:.3em; color:var(--ink-dim); }
  .slider input[type=range]{ -webkit-appearance:none; width:120px; height:2px;
    background:var(--line); outline:none; }
  .slider input[type=range]::-webkit-slider-thumb{ -webkit-appearance:none; width:10px; height:16px;
    background:#e6cf9a; cursor:pointer; border:none; }
  .slider input[type=range]::-moz-range-thumb{ width:10px; height:16px; border:none;
    border-radius:0; background:#e6cf9a; cursor:pointer; }

  #hint{ position:fixed; bottom:14px; left:50%; transform:translateX(-50%); z-index:10;
    font-size:10px; letter-spacing:.4em; color:rgba(205,177,125,.35);
    opacity:0; transition:opacity 1.5s ease 1.8s; white-space:nowrap; }
  body.ready #hint{ opacity:1; }

  #err{ position:fixed; inset:0; z-index:50; display:none; align-items:center; justify-content:center;
    background:rgba(8,6,4,.9); }
  #err .box{ max-width:480px; margin:20px; border:1px solid rgba(205,177,125,.4);
    background:#151009; padding:26px 30px; }
  #err .t{ font-size:18px; letter-spacing:.3em; color:#e6cf9a; margin-bottom:14px; }
  #err .d{ font-size:12px; line-height:1.9; color:rgba(205,177,125,.78);
    letter-spacing:.08em; word-break:break-all; white-space:pre-wrap; }
  #err .tip{ margin-top:16px; padding-top:12px; border-top:1px solid rgba(205,177,125,.2);
    font-size:10px; letter-spacing:.25em; color:rgba(205,177,125,.4); }

  @media (max-width:760px){
    #brand{ top:18px; left:18px; padding:14px 16px; }
    #brand h1{ font-size:28px; }
    #readout{ left:18px; bottom:16px; }
    #ctrl{ right:18px; bottom:16px; }
    #hint{ display:none; }
  }
</style>
</head>
<body>

<div id="brand">
  <div class="tag">标本编号 · SPECIMEN M-09</div>
  <h1>机械凤蝶</h1>
  <div class="latin">Machina papilio · var. horologia</div>
  <div class="note">黄铜 / 青铜 / 弹簧钢 — 全部机构程序化锻造<br>翅脉 41 式 · 齿轮传动比 22 : 10 · 无外部模型</div>
</div>

<div id="readout">
  <div class="cap">机构运转 · MECHANISM</div>
  <div class="row">扑翼 <b id="hz">0.0</b> Hz</div>
  <div class="row">相位 <b id="ph">000</b> °</div>
  <div class="row">状态 <b id="st">巡航</b></div>
</div>

<div id="ctrl">
  <div class="slider">扇频<input type="range" id="speed" min="40" max="200" value="100"></div>
  <button id="bHover">悬停</button>
  <button id="bClose">特写</button>
  <button id="bOrbit">环绕</button>
  <button id="bWide">全景</button>
</div>

<div id="hint">拖 拽 旋 转 &nbsp;·&nbsp; 滚 轮 缩 放</div>

<div id="err">
  <div class="box">
    <div class="t">出错了</div>
    <div class="d"></div>
    <div class="tip">按 F12 打开控制台可查看详细日志 · 刷新页面重试</div>
  </div>
</div>

<div class="vignette"></div>

<!-- 全局兜底:模块失败也能报错而不是黑屏 -->
<script>
  window.__ok = false;
  window.__fatalShown = false;
  window.__fatal = function(title, detail){
    window.__fatalShown = true;
    var el = document.getElementById('err');
    if(!el) return;
    el.style.display = 'flex';
    el.querySelector('.t').textContent = title || '出错了';
    el.querySelector('.d').textContent = detail || '';
  };
  setTimeout(function(){
    if(!window.__ok && !window.__fatalShown){
      window.__fatal('引擎未能启动',
        '15 秒内未完成初始化。最可能的原因:\n1) three.js 库(CDN)被网络拦截或超时;\n2) 浏览器过旧,不支持 ES 模块。');
    }
  }, 15000);
  window.addEventListener('error', function(e){
    if(e && e.error && !window.__fatalShown){
      window.__fatal('运行时错误', (e.message || '') + '');
    }
  });
</script>

<!-- 主模块:动态加载 three(多 CDN 回退,无 importmap,无附加模块) -->
<script type="module">
const CDN_LIST = [
  'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.min.js',
  'https://unpkg.com/three@0.160.0/build/three.module.min.js',
  'https://cdn.staticfile.org/three.js/0.160.0/three.module.min.js',
  'https://cdn.bootcdn.net/ajax/libs/three.js/0.160.0/three.module.min.js',
  'https://cdnjs.cloudflare.com/ajax/libs/three.js/0.160.0/three.module.min.js',
  'https://esm.sh/three@0.160.0',
];
function importWithTimeout(url, ms){
  return Promise.race([
    import(url),
    new Promise((_,rej)=>setTimeout(()=>rej(new Error('timeout')), ms))
  ]);
}
async function loadThree(){
  for(const url of CDN_LIST){
    try{
      const mod = await importWithTimeout(url, 5000);
      if(mod && mod.Scene){ console.log('[机械凤蝶] Three.js 加载自:', url); return mod; }
    }catch(err){ console.warn('[机械凤蝶] CDN 不可用:', url, err && err.message); }
  }
  return null;
}
loadThree().then(THREE=>{
  if(!THREE){
    window.__fatal('Three.js 库加载失败', '已尝试 ' + CDN_LIST.length + ' 个 CDN 均未成功,请检查网络后刷新。');
    return;
  }
  try{ main(THREE); }
  catch(e){ console.error(e); window.__fatal('初始化时出错', (e && e.stack) || String(e)); }
});

function main(THREE){

/* ---------- 一、舞台 ---------- */
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0b0a08);
scene.fog = new THREE.FogExp2(0x0b0a08, 0.028);

const camera = new THREE.PerspectiveCamera(55, innerWidth/innerHeight, 0.1, 120);
camera.position.set(16, 7, 18);

let renderer;
try{ renderer = new THREE.WebGLRenderer({ antialias:true }); }
catch(e){ window.__fatal('WebGL 不可用', '浏览器未能创建 WebGL 上下文:' + (e && e.message || e)); return; }
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.toneMapping = THREE.NoToneMapping;   // 色调映射在自研合成着色器中执行
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);

/* ---------- 二、环境反射(手造灯房) ---------- */
{
  const pmrem = new THREE.PMREMGenerator(renderer);
  const env = new THREE.Scene();
  env.add(new THREE.Mesh(
    new THREE.BoxGeometry(12,12,12),
    new THREE.MeshBasicMaterial({ color:0x181209, side:THREE.BackSide })));
  const panel = (color, k, w, h, x, y, z)=>{
    const m = new THREE.Mesh(new THREE.PlaneGeometry(w, h),
      new THREE.MeshBasicMaterial({ color:new THREE.Color(color).multiplyScalar(k) }));
    m.position.set(x,y,z); m.lookAt(0,0,0); env.add(m);
  };
  panel(0xfff0d2, 7.0, 5, 2.4,  0,   5,  3);
  panel(0x5fd8cc, 3.5, 4, 1.6, -5,  0.5, -4);
  panel(0xffb14a, 2.5, 3, 1.2,  4.5, -1.5, 4);
  panel(0x8c6a3d, 1.4, 4, 1.5,  5,   1,  -3);
  scene.environment = pmrem.fromScene(env, 0.04).texture;   // 0.04:不再触发采样数警告
  pmrem.dispose();
}

/* ---------- 三、灯光 ---------- */
const key = new THREE.SpotLight(0xffdfb0, 340, 45, 0.55, 0.9, 1.6);
key.position.set(7, 10, 6);
key.castShadow = true;
key.shadow.mapSize.set(1024, 1024);
key.shadow.bias = -0.0003;
key.shadow.camera.near = 3; key.shadow.camera.far = 30;
scene.add(key);
const rim = new THREE.DirectionalLight(0x5fd8cc, 1.7); rim.position.set(-6, 3, -7); scene.add(rim);
scene.add(new THREE.HemisphereLight(0x55462d, 0x0c0a08, 0.6));

/* ---------- 四、材质库 ---------- */
const M = {
  brass:    new THREE.MeshStandardMaterial({ color:0xc09a5f, metalness:1,   roughness:.34, envMapIntensity:.75 }),
  brassDark:new THREE.MeshStandardMaterial({ color:0x77572e, metalness:1,   roughness:.48, envMapIntensity:.6  }),
  bronze:   new THREE.MeshStandardMaterial({ color:0x8c6a3d, metalness:.95, roughness:.42, envMapIntensity:.65 }),
  steel:    new THREE.MeshStandardMaterial({ color:0xb8c0c8, metalness:1,   roughness:.24, envMapIntensity:.8  }),
  ironDark: new THREE.MeshStandardMaterial({ color:0x2c2620, metalness:.9,  roughness:.62, envMapIntensity:.4  }),
  glowAmber:new THREE.MeshStandardMaterial({ color:0x140b04, emissive:0xffb14a, emissiveIntensity:2.6 }),
  glowCyan: new THREE.MeshStandardMaterial({ color:0x06231f, emissive:0x59f2d8, emissiveIntensity:1.7, flatShading:true }),
};

/* ---------- 五、工具 ---------- */
const lerp=(a,b,k)=>a+(b-a)*k;
const clamp=(x,a,b)=>Math.max(a,Math.min(b,x));
const UP = new THREE.Vector3(0,1,0);
const _v = new THREE.Vector3(), _m4 = new THREE.Matrix4(), _q = new THREE.Quaternion();
const _p0 = new THREE.Vector3(), _p1 = new THREE.Vector3(), _dir = new THREE.Vector3();
const _zero = new THREE.Vector3();

function makeNut(r,h){ return new THREE.CylinderGeometry(r,r,h,6); }

function makeGear(teeth, module, thickness=0.05, spokes=5, boreR=null){
  const r = module * teeth / 2;
  const rOut = r + module, rRoot = r - module * 1.15;
  boreR = boreR ?? Math.max(0.012, r*0.16);
  const shape = new THREE.Shape();
  const step = Math.PI*2/teeth;
  for(let i=0;i<teeth;i++){
    const a=i*step;
    if(i===0) shape.moveTo(Math.cos(a)*rRoot, Math.sin(a)*rRoot);
    shape.absarc(0,0,rRoot, a, a+step*0.18, false);
    shape.lineTo(Math.cos(a+step*0.42)*rOut, Math.sin(a+step*0.42)*rOut);
    shape.absarc(0,0,rOut, a+step*0.42, a+step*0.60, false);
    shape.lineTo(Math.cos(a+step)*rRoot, Math.sin(a+step)*rRoot);
  }
  shape.closePath();
  const bore = new THREE.Path(); bore.absarc(0,0,boreR,0,Math.PI*2,true); shape.holes.push(bore);
  if(spokes>0){
    const r1=boreR*2.2, r2=rRoot*0.8;
    for(let s=0;s<spokes;s++){
      const a=s/spokes*Math.PI*2+0.28, w=Math.PI*2/spokes*0.52;
      const p=new THREE.Path();
      p.absarc(0,0,r2, a, a+w, false);
      p.absarc(0,0,r1, a+w, a, true);
      p.closePath();
      shape.holes.push(p);
    }
  }
  const g = new THREE.ExtrudeGeometry(shape, { depth:thickness, bevelEnabled:true,
    bevelThickness:thickness*0.14, bevelSize:module*0.1, bevelSegments:1, curveSegments:4 });
  g.center();
  return g;
}

function mergeGeos(list){
  const parts = list.map(g => g.index ? g.toNonIndexed() : g);
  let total = 0;
  for(const g of parts) total += g.attributes.position.count;
  const pos = new Float32Array(total*3), nor = new Float32Array(total*3), uv = new Float32Array(total*2);
  let o = 0;
  for(const g of parts){
    pos.set(g.attributes.position.array, o*3);
    nor.set(g.attributes.normal.array, o*3);
    if(g.attributes.uv) uv.set(g.attributes.uv.array, o*2);
    o += g.attributes.position.count;
  }
  const out = new THREE.BufferGeometry();
  out.setAttribute('position', new THREE.BufferAttribute(pos,3));
  out.setAttribute('normal',   new THREE.BufferAttribute(nor,3));
  out.setAttribute('uv',       new THREE.BufferAttribute(uv,2));
  return out;
}

/* ---------- 六、翅膀设计图(凤蝶脉式) ---------- */
const FORE_SHAPE = [
  ['M',[0,.04]],
  ['B',[.16,.24,.55,.47,1.0,.57]],
  ['B',[1.05,.55,1.06,.48,1.02,.40]],
  ['B',[.90,.12,.66,-.10,.40,-.155]],
  ['B',[.24,-.175,.10,-.13,.03,-.06]],
];
const HIND_SHAPE = [
  ['M',[0,.05]],
  ['B',[.22,.26,.50,.27,.72,.10]],
  ['B',[.88,-.02,.86,-.20,.74,-.30]],
  ['B',[.70,-.36,.76,-.50,.85,-.68]],
  ['B',[.92,-.85,.97,-.98,.99,-1.04]],
  ['B',[1.01,-1.09,.99,-1.115,.95,-1.09]],
  ['B',[.88,-1.05,.83,-.97,.79,-.88]],
  ['B',[.72,-.72,.68,-.58,.64,-.46]],
  ['B',[.52,-.30,.30,-.20,.10,-.13]],
  ['B',[.02,-.09,-.01,-.01,0,.05]],
];
const FORE_BOX = { x0:0, x1:1.06, y0:-.18, y1:.58 };
const HIND_BOX = { x0:-.02, x1:1.02, y0:-1.13, y1:.29 };

const FORE_VEINS = [
  {id:'A', p0:[.02,.015],  c:[.42,.30],  p1:[.99,.565], r:.0062, seg:30},
  {id:'B', p0:[.03,.005],  c:[.40,.205], p1:[.965,.50], r:.0068, seg:30},
  {id:'C', p0:[.03,-.005], c:[.40,.10],  p1:[.915,.34], r:.0068, seg:30},
  {id:'D', p0:[.025,-.015],c:[.38,.02],  p1:[.815,.14], r:.0062, seg:28},
  {id:'E', p0:[.02,-.025], c:[.36,-.02], p1:[.66,0],    r:.0056, seg:26},
  {id:'F', p0:[.02,-.035], c:[.33,-.06], p1:[.51,-.09],r:.005,  seg:24},
  {id:'G', p0:[.02,-.045], c:[.28,-.10], p1:[.36,-.135],r:.0045,seg:22},
  {id:'H', p0:[.02,-.052], c:[.18,-.11], p1:[.20,-.148],r:.0038,seg:18},
  {from:'B', t:.55, c:[.64,.30],  p1:[.90,.415],  r:.0036, seg:18},
  {from:'B', t:.72, c:[.74,.27],  p1:[.945,.44],  r:.0032, seg:14},
  {from:'C', t:.55, c:[.60,.135], p1:[.868,.215], r:.0036, seg:18},
  {from:'C', t:.70, c:[.63,.05],  p1:[.79,.055],  r:.0032, seg:14},
  {from:'D', t:.58, c:[.55,-.03], p1:[.72,.015],  r:.0034, seg:16},
  {from:'D', t:.42, c:[.47,-.055],p1:[.575,-.075],r:.003,  seg:12},
  {from:'E', t:.60, c:[.44,-.075],p1:[.585,-.115],r:.003,  seg:12},
  {from:'F', t:.55, c:[.30,-.10], p1:[.40,-.145], r:.0028, seg:10},
  {xv:'B', xt:.42, yv:'C', yt:.44, r:.003},
  {xv:'D', xt:.42, yv:'E', yt:.40, r:.0028},
  {xv:'F', xt:.40, yv:'G', yt:.42, r:.0026},
];
const HIND_VEINS = [
  {id:'A', p0:[.02,.055], c:[.35,.235], p1:[.755,.125], r:.0058, seg:28},
  {id:'B', p0:[.02,.03],  c:[.35,.12],  p1:[.72,-.02],  r:.0062, seg:28},
  {id:'C', p0:[.02,.005], c:[.33,-.02], p1:[.62,-.18],  r:.0058, seg:26},
  {id:'D', p0:[.02,-.015],c:[.30,-.12], p1:[.50,-.345], r:.0055, seg:26},
  {id:'E', p0:[.02,-.032],c:[.26,-.18], p1:[.345,-.50], r:.005,  seg:24},
  {id:'F', p0:[.02,-.04], c:[.20,-.20], p1:[.245,-.615],r:.0045, seg:22},
  {from:'D', t:.78, c:[.66,-.53], p1:[.925,-.905], r:.0042, seg:22},
  {from:'D', t:.62, c:[.56,-.46], p1:[.775,-.85],  r:.0036, seg:20},
  {from:'D', t:.45, c:[.36,-.28], p1:[.435,-.375],r:.0034, seg:14},
  {from:'C', t:.55, c:[.44,-.10], p1:[.555,-.195],r:.0034, seg:14},
  {from:'B', t:.52, c:[.50,.055], p1:[.685,.03],  r:.0034, seg:12},
  {from:'E', t:.60, c:[.30,-.34], p1:[.40,-.45],  r:.003,  seg:10},
  {xv:'A', xt:.5, yv:'B', yt:.5, r:.0026},
  {xv:'C', xt:.45, yv:'D', yt:.45, r:.0026},
];

/* ---------- 七、翅膜纹理 ---------- */
function buildShape(cmds, side){
  const s = new THREE.Shape();
  const X = x=>x*side;
  for(const [t,p] of cmds){
    if(t==='M') s.moveTo(X(p[0]), p[1]);
    else s.bezierCurveTo(X(p[0]),p[1],X(p[2]),p[3],X(p[4]),p[5]);
  }
  s.closePath();
  return s;
}
function drawWingTexture(kind){
  const S=1024;
  const cv=document.createElement('canvas'); cv.width=cv.height=S;
  const ctx=cv.getContext('2d');
  const bx = kind==='fore'?FORE_BOX:HIND_BOX;
  const X=x=>(x-bx.x0)/(bx.x1-bx.x0)*S;
  const Y=y=>(1-(y-bx.y0)/(bx.y1-bx.y0))*S;
  const outline = buildShape(kind==='fore'?FORE_SHAPE:HIND_SHAPE, 1).getSpacedPoints(120);
  const root = { x:X(.02), y:Y(kind==='fore'?0:.01) };

  ctx.fillStyle = kind==='fore' ? 'rgba(148,92,40,.94)' : 'rgba(118,76,32,.94)';
  ctx.fillRect(0,0,S,S);
  ctx.strokeStyle='rgba(255,212,145,.07)'; ctx.lineWidth=2.5;
  for(let i=0;i<64;i++){
    const p=outline[(i*120/64|0)];
    ctx.beginPath(); ctx.moveTo(root.x,root.y); ctx.lineTo(X(p.x),Y(p.y)); ctx.stroke();
  }
  let g=ctx.createRadialGradient(root.x,root.y,S*0.12,root.x,root.y,S*0.95);
  g.addColorStop(0,'rgba(18,8,2,0)'); g.addColorStop(1,'rgba(18,8,2,.6)');
  ctx.fillStyle=g; ctx.fillRect(0,0,S,S);
  ctx.strokeStyle='rgba(255,216,152,.5)'; ctx.lineWidth=22; ctx.lineJoin='round';
  ctx.beginPath();
  const n = kind==='fore'?36:30;
  for(let i=0;i<=n;i++){ const p=outline[i]; i?ctx.lineTo(X(p.x),Y(p.y)):ctx.moveTo(X(p.x),Y(p.y)); }
  ctx.stroke();

  if(kind==='fore'){
    ctx.strokeStyle='rgba(255,188,105,.26)'; ctx.lineWidth=30;
    ctx.beginPath(); ctx.moveTo(X(.18),Y(.30)); ctx.quadraticCurveTo(X(.5),Y(.24),X(.82),Y(.28)); ctx.stroke();
    ctx.beginPath(); ctx.moveTo(X(.16),Y(.14)); ctx.quadraticCurveTo(X(.5),Y(.06),X(.78),Y(.10)); ctx.stroke();
    ctx.fillStyle='rgba(246,238,220,.88)';
    for(let i=0;i<7;i++){
      const p=outline[52+i*7];
      ctx.beginPath(); ctx.arc(X(p.x),Y(p.y),7,0,Math.PI*2); ctx.fill();
    }
  }else{
    g=ctx.createRadialGradient(X(.9),Y(-1.0),0, X(.9),Y(-1.0),S*0.34);
    g.addColorStop(0,'rgba(255,182,92,.42)'); g.addColorStop(1,'rgba(255,182,92,0)');
    ctx.fillStyle=g; ctx.fillRect(0,0,S,S);
    const ex=X(.44), ey=Y(-.33);
    const ring=(r,c)=>{ ctx.fillStyle=c; ctx.beginPath(); ctx.ellipse(ex,ey,r,r*0.82,0,0,Math.PI*2); ctx.fill(); };
    ring(120,'rgba(28,58,50,.9)');
    ring(80,'rgba(232,172,80,.92)');
    ring(50,'rgba(14,10,6,.96)');
    ring(26,'rgba(118,220,204,.95)');
    ctx.fillStyle='rgba(250,250,244,.95)';
    ctx.beginPath(); ctx.ellipse(ex-30,ey-22,8,6,-.5,0,Math.PI*2); ctx.fill();
    ctx.fillStyle='rgba(112,206,188,.7)';
    for(let i=0;i<3;i++){
      ctx.beginPath(); ctx.ellipse(X(.18+i*.16),Y(.14),11,8,0,0,Math.PI*2); ctx.fill();
    }
  }
  ctx.globalCompositeOperation='destination-out';
  g=ctx.createRadialGradient(root.x,root.y,S*0.45,root.x,root.y,S);
  g.addColorStop(0,'rgba(0,0,0,0)'); g.addColorStop(1,'rgba(0,0,0,.42)');
  ctx.fillStyle=g; ctx.fillRect(0,0,S,S);
  ctx.globalCompositeOperation='source-over';

  const tex=new THREE.CanvasTexture(cv);
  tex.colorSpace=THREE.SRGBColorSpace; tex.anisotropy=8;
  return tex;
}

/* ---------- 八、翅膀构建 ---------- */
function makeMembraneGeo(shape, rootX, rootY, K, rows, box){
  const pts = shape.getSpacedPoints(K);
  const pos=[], uv=[], idx=[];
  for(let i=0;i<=K;i++){
    const p=pts[i];
    for(let m=0;m<=rows;m++){
      const s=(m/rows)*0.995;
      const x=rootX+(p.x-rootX)*s, y=rootY+(p.y-rootY)*s;
      pos.push(x,y,0);
      uv.push((x-box.x0)/(box.x1-box.x0), (y-box.y0)/(box.y1-box.y0));
    }
  }
  const W=rows+1;
  for(let i=0;i<K;i++) for(let m=0;m<rows;m++){
    const a=i*W+m, b=(i+1)*W+m;
    idx.push(a,b,a+1, b,b+1,a+1);
  }
  const g=new THREE.BufferGeometry();
  g.setAttribute('position', new THREE.Float32BufferAttribute(pos,3));
  g.setAttribute('uv', new THREE.Float32BufferAttribute(uv,2));
  g.setIndex(idx);
  g.computeVertexNormals();
  return g;
}
function buildVeinsGeo(spec, side, shape, rivets){
  const geos=[], curves={};
  const V=(x,y)=>new THREE.Vector3(x*side,y,0);
  for(const v of spec){
    /* ★ 修复:横脉条目没有 p0,必须最先分流处理(原先放在 p0 计算之后导致崩溃) */
    if(v.xv){
      const a=curves[v.xv].getPoint(v.xt), b=curves[v.yv].getPoint(v.yt);
      const mid=a.clone().add(b).multiplyScalar(.5);
      const dir=b.clone().sub(a); const nrm=new THREE.Vector3(-dir.y,dir.x,0).normalize();
      const c1=a.clone().lerp(mid,.5).addScaledVector(nrm,.09);
      const c2=b.clone().lerp(mid,.5).addScaledVector(nrm,.09);
      geos.push(new THREE.TubeGeometry(new THREE.CubicBezierCurve3(a,c1,c2,b),14,v.r,5,false));
      continue;
    }
    let p0;
    if(v.from){ p0=curves[v.from].getPoint(v.t); }
    else p0=V(v.p0[0], v.p0[1]);
    const c=new THREE.QuadraticBezierCurve3(p0, V(v.c[0],v.c[1]), V(v.p1[0],v.p1[1]));
    if(v.id) curves[v.id]=c;
    geos.push(new THREE.TubeGeometry(c, v.seg||20, v.r, 5, false));
  }
  const pts=shape.getSpacedPoints(70).map(p=>new THREE.Vector3(p.x*side,p.y,0));
  geos.push(new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts,true,'catmullrom',.5),150,.0085,5,true));
  for(const [rx,ry] of rivets){
    const nut=makeNut(.014,.02); nut.rotateX(Math.PI/2); nut.translate(rx*side,ry,0);
    const cap=makeNut(.021,.006); cap.rotateX(Math.PI/2); cap.translate(rx*side,ry,-.012);
    geos.push(nut,cap);
  }
  return mergeGeos(geos);
}

const texFore=drawWingTexture('fore'), texHind=drawWingTexture('hind');
const memMatFore=new THREE.MeshPhysicalMaterial({
  map:texFore, transparent:true, opacity:.93, depthWrite:true, side:THREE.DoubleSide,
  metalness:.55, roughness:.34, iridescence:1, iridescenceIOR:1.32, iridescenceThicknessRange:[140,460],
  emissive:0x9a5a20, emissiveMap:texFore, emissiveIntensity:.22, envMapIntensity:.7 });
const memMatHind=memMatFore.clone(); memMatHind.map=texHind; memMatHind.emissiveMap=texHind;

function buildWing(side, kind){
  const isFore = kind==='fore';
  const box = isFore?FORE_BOX:HIND_BOX;
  const scale = isFore?1.55:1.22;
  const shape = buildShape(isFore?FORE_SHAPE:HIND_SHAPE, side);
  const rootX = .015, rootY = isFore?.005:.01;

  const memGeo = makeMembraneGeo(shape, rootX, rootY, 64, 16, box);
  memGeo.scale(scale,scale,1);
  const mem = new THREE.Mesh(memGeo, isFore?memMatFore:memMatHind);

  const rivets = isFore ? [[.35,.18],[.55,.14],[.30,-.05],[.50,-.02]] : [[.30,.10],[.35,-.15],[.45,-.28]];
  const veinsGeo = buildVeinsGeo(isFore?FORE_VEINS:HIND_VEINS, side, shape, rivets);
  veinsGeo.scale(scale,scale,1);
  const veins = new THREE.Mesh(veinsGeo, M.brass);
  veins.castShadow = true;

  const N = 88;
  const sc = new THREE.InstancedMesh(new THREE.BoxGeometry(.034,.062,.01), M.brassDark, N);
  sc.castShadow = true;
  const outPts = shape.getSpacedPoints(N-1);
  const center = outPts.reduce((a,p)=>a.addScaledVector(p,1), new THREE.Vector2()).divideScalar(N);
  const dummy = new THREE.Object3D();
  const scData = [];
  for(let i=0;i<N;i++){
    const p=outPts[i], q=outPts[(i+1)%N];
    let n=new THREE.Vector2(-(q.y-p.y), q.x-p.x);
    if(n.x*(p.x-center.x)+n.y*(p.y-center.y)<0) n.negate();
    n.normalize();
    dummy.position.set((p.x+n.x*.028)*scale, (p.y+n.y*.028)*scale, -.006);
    dummy.lookAt(dummy.position.x+n.x*side, dummy.position.y+n.y, -.006);
    dummy.rotateX(.35); dummy.rotateZ((i%2)*.09-.05);
    dummy.updateMatrix();
    sc.setMatrixAt(i, dummy.matrix);
    scData.push({ x:p.x*scale, y:p.y*scale, bz:-.006 });
  }

  const deco = new THREE.Group();
  const dg = new THREE.Mesh(makeGear(isFore?14:20, isFore?.010:.013, .016, 4), M.bronze);
  dg.position.set((isFore?.58:.40)*scale*side, (isFore?.24:-.17)*scale, -.05);
  dg.userData.spin = (Math.random()<.5?1:-1) * (0.6+Math.random()*0.8);
  const dg2 = new THREE.Mesh(makeGear(9,.011,.014,0), M.steel);
  dg2.position.set(dg.position.x+.16*side, dg.position.y-.1, -.05);
  dg2.userData.spin = (Math.random()<.5?1:-1) * (0.6+Math.random()*0.8);
  deco.add(dg,dg2);

  const wingFlat = new THREE.Group();
  wingFlat.rotation.x = Math.PI/2;
  wingFlat.add(mem, veins, sc, deco);

  const tip = new THREE.Object3D();
  tip.position.set(side*(isFore?1.5:1.1), isFore?.7:-1.2, 0);
  wingFlat.add(tip);

  const targets = [
    { geo:memGeo,   base:memGeo.attributes.position.array.slice(),   isMem:true },
    { geo:veinsGeo, base:veinsGeo.attributes.position.array.slice(), isMem:false },
  ];
  const span = isFore?1.62:1.26;
  for(const tg of targets){
    const b=tg.base;
    for(let i=0;i<b.length;i+=3){
      const u=Math.min(1,Math.abs(b[i])/span);
      b[i+2] += Math.sin(u*Math.PI)*(isFore?.05:.045) + b[i+1]*u*.05;
    }
  }
  return { wingFlat, sc, scData, targets, decoGears:[dg,dg2], span, flex:isFore?.14:.10, tip };
}

/* ---------- 九、蝴蝶总装 ---------- */
function buildButterfly(){
  const root = new THREE.Group();
  const body = new THREE.Group();
  root.add(body);
  const refs = { wings:[], shoulders:[], gearsMain:[], gearsSide:[], decoGears:[], abdomen:[], legs:[], tips:[] };

  const thorax = new THREE.Mesh(new THREE.SphereGeometry(1,26,20), M.bronze);
  thorax.scale.set(.27,.30,.46); thorax.position.set(0,.14,.16);
  thorax.castShadow=true; body.add(thorax);
  const ridge = new THREE.Mesh(new THREE.CapsuleGeometry(.022,.5,4,10), M.brass);
  ridge.rotation.x=Math.PI/2; ridge.position.set(0,.33,.14); body.add(ridge);
  for(const s of [1,-1]){
    const plate=new THREE.Mesh(new THREE.SphereGeometry(1,18,14), M.brassDark);
    plate.scale.set(.09,.12,.20); plate.position.set(s*.21,.10,.14); body.add(plate);
  }
  const core = new THREE.Mesh(new THREE.OctahedronGeometry(.085,0), M.glowAmber);
  core.position.set(0,.25,.33); body.add(core);
  const cage = new THREE.Group(); cage.position.copy(core.position);
  for(const r of [0,Math.PI/2,Math.PI/4]){
    const t=new THREE.Mesh(new THREE.TorusGeometry(.12,.007,8,40), M.steel);
    t.rotation.set(r, r*.6, 0); cage.add(t);
  }
  body.add(cage);
  const coreLight = new THREE.PointLight(0xffb14a, 1.6, 2.4, 2);
  coreLight.position.copy(core.position); body.add(coreLight);

  const head = new THREE.Group(); head.position.set(0,.17,.55); body.add(head);
  const skull = new THREE.Mesh(new THREE.SphereGeometry(.155,22,18), M.bronze);
  skull.castShadow=true; head.add(skull);
  const crest = new THREE.Mesh(new THREE.SphereGeometry(.10,16,12,0,Math.PI*2,0,1.1), M.brass);
  crest.position.set(0,.07,.02); crest.rotation.x=-.5; head.add(crest);
  for(const s of [1,-1]){
    const eye=new THREE.Mesh(new THREE.IcosahedronGeometry(.075,1), M.glowCyan);
    eye.position.set(s*.098,.03,.075); head.add(eye);
    const pts=[new THREE.Vector3(s*.05,.12,.05),new THREE.Vector3(s*.09,.24,.19),
               new THREE.Vector3(s*.06,.35,.34),new THREE.Vector3(s*.10,.40,.48)];
    const curve=new THREE.CatmullRomCurve3(pts);
    head.add(new THREE.Mesh(new THREE.TubeGeometry(curve,24,.0075,6), M.brass));
    const club=new THREE.Mesh(new THREE.CapsuleGeometry(.019,.055,4,8), M.brassDark);
    club.position.copy(curve.getPoint(1));
    club.quaternion.setFromUnitVectors(UP, curve.getTangent(1));
    head.add(club);
  }
  const prob=[];
  for(let k=0;k<=42;k++){
    const a=k/42*Math.PI*2*4.5, r=.008+(k/42)*.05;
    prob.push(new THREE.Vector3(Math.cos(a)*r, -.065-k*.0006, .15+Math.sin(a)*r*.85));
  }
  head.add(new THREE.Mesh(new THREE.TubeGeometry(new THREE.CatmullRomCurve3(prob),120,.0048,5), M.steel));

  const abd=new THREE.Group(); abd.position.set(0,.03,0); body.add(abd);
  for(let i=0;i<9;i++){
    const r=.132*(1-i*.088);
    const seg=new THREE.Mesh(new THREE.SphereGeometry(1,18,14), i%2?M.brassDark:M.bronze);
    seg.scale.set(r*.94,r*.86,r*1.3); seg.position.z=-.10-i*.105;
    seg.castShadow=true; abd.add(seg);
    if(i<8){
      const ring=new THREE.Mesh(new THREE.TorusGeometry(r*.72,.006,6,26), M.glowAmber.clone());
      ring.material.emissiveIntensity=.9;
      ring.position.z=-.10-(i+.5)*.105; abd.add(ring);
    }
    refs.abdomen.push(seg);
  }
  const tail=new THREE.Mesh(new THREE.ConeGeometry(.024,.09,10), M.steel);
  tail.rotation.x=-Math.PI/2; tail.position.z=-1.02; abd.add(tail);

  function makeLeg(){
    const g=new THREE.Group();
    const hip=new THREE.Mesh(new THREE.SphereGeometry(.028,10,8), M.steel); g.add(hip);
    const femur=new THREE.Mesh(new THREE.CapsuleGeometry(.019,.16,4,8), M.brassDark);
    femur.position.y=-.08; femur.castShadow=true;
    const femurG=new THREE.Group(); femurG.rotation.x=-.95; femurG.add(femur);
    const knee=new THREE.Mesh(new THREE.SphereGeometry(.022,10,8), M.steel);
    knee.position.y=-.16; femurG.add(knee); g.add(femurG);
    const tibiaG=new THREE.Group(); tibiaG.position.y=-.16; tibiaG.rotation.x=1.8;
    const tibia=new THREE.Mesh(new THREE.CapsuleGeometry(.012,.18,4,8), M.steel);
    tibia.position.y=-.09; tibia.castShadow=true; tibiaG.add(tibia);
    const tarsG=new THREE.Group(); tarsG.position.y=-.18; tarsG.rotation.x=.85;
    const tarsus=new THREE.Mesh(new THREE.CapsuleGeometry(.007,.08,4,6), M.bronze);
    tarsus.position.y=-.04; tarsG.add(tarsus);
    const claw=new THREE.Mesh(new THREE.ConeGeometry(.008,.028,6), M.steel);
    claw.position.y=-.085; claw.rotation.x=Math.PI; tarsG.add(claw);
    tibiaG.add(tarsG); femurG.add(tibiaG);
    g.add(femurG);
    return g;
  }
  const legPos=[[.32,-.9],[.12,-.5],[-.08,-.15]];
  for(let i=0;i<3;i++) for(const s of [1,-1]){
    const leg=makeLeg();
    leg.position.set(s*.13,-.02,legPos[i][0]);
    leg.rotation.y=s*legPos[i][1]; leg.rotation.z=s*.3;
    body.add(leg); refs.legs.push(leg);
  }

  const wingDefs=[ {kind:'fore', z:.20, x:.15, off:0},
                   {kind:'hind', z:-.05, x:.13, off:.55} ];
  for(const d of wingDefs) for(const s of [1,-1]){
    const roll=new THREE.Group(); roll.position.set(s*d.x,.235,d.z);
    const tilt=new THREE.Group();
    const w=buildWing(s,d.kind);
    tilt.add(w.wingFlat); roll.add(tilt); body.add(roll);
    const gear=new THREE.Mesh(makeGear(22,.0125,.045,5), M.brass);
    gear.position.set(s*.02,.055,.02); gear.castShadow=true;
    roll.add(gear);
    const pin=new THREE.Mesh(makeGear(10,.0125,.05,0), M.bronze);
    pin.position.set(s*.225,.155,d.z); body.add(pin);
    const cap=new THREE.Mesh(new THREE.ConeGeometry(.075,.13,10), M.brass);
    cap.position.set(s*d.x,.30,d.z); body.add(cap);

    refs.shoulders.push({ roll, tilt, side:s, off:d.off });
    refs.gearsMain.push({ mesh:gear, dir:s });
    refs.gearsSide.push({ mesh:pin,  dir:-s });
    refs.wings.push(w);
    refs.tips.push(w.tip);
    refs.decoGears.push(...w.decoGears);
  }
  return { root, body, refs, core, coreLight, cage, head };
}
const butterfly=buildButterfly();
scene.add(butterfly.root);

/* ---------- 十、环境 ---------- */
const backdrop=new THREE.Group(); scene.add(backdrop);

function makeSprite(){
  const c=document.createElement('canvas'); c.width=c.height=64;
  const x=c.getContext('2d');
  const g=x.createRadialGradient(32,32,0,32,32,30);
  g.addColorStop(0,'rgba(255,255,255,1)'); g.addColorStop(.4,'rgba(255,255,255,.4)');
  g.addColorStop(1,'rgba(255,255,255,0)');
  x.fillStyle=g; x.fillRect(0,0,64,64);
  const t=new THREE.CanvasTexture(c); t.colorSpace=THREE.SRGBColorSpace; return t;
}
const spriteTex=makeSprite();

const dial=new THREE.Group(); dial.position.set(0,.1,-7.2); backdrop.add(dial);
const backplate=new THREE.Mesh(new THREE.CircleGeometry(7.7,96),
  new THREE.MeshStandardMaterial({color:0x14100c,metalness:.3,roughness:.9,envMapIntensity:.25}));
backplate.position.z=-.28; dial.add(backplate);
dial.add(new THREE.Mesh(new THREE.TorusGeometry(7.5,.09,10,150), M.brassDark));
dial.add(new THREE.Mesh(new THREE.TorusGeometry(6.7,.035,8,130), M.bronze));
{
  const gs=[];
  for(let i=0;i<60;i++){
    const major=i%5===0;
    const b=new THREE.BoxGeometry(major?.085:.05, major?.5:.27, .05);
    b.translate(0, major?6.95:7.1, 0);
    b.rotateZ(i/60*Math.PI*2);
    gs.push(b);
  }
  dial.add(new THREE.Mesh(mergeGeos(gs), M.brass));
}
for(let i=0;i<4;i++){
  const dot=new THREE.Mesh(new THREE.SphereGeometry(.07,10,8), M.glowAmber.clone());
  dot.material.emissiveIntensity=1.1;
  const a=i*Math.PI/2+Math.PI/4;
  dot.position.set(Math.cos(a)*5.6, Math.sin(a)*5.6, .1);
  dial.add(dot);
}
const hub=new THREE.Mesh(new THREE.CylinderGeometry(.55,.62,.2,24), M.bronze);
hub.rotation.x=Math.PI/2; dial.add(hub);
function makeHand(len,w){
  const s=new THREE.Shape();
  s.moveTo(-w,0); s.lineTo(-w*.5,len*.92); s.lineTo(0,len);
  s.lineTo(w*.5,len*.92); s.lineTo(w,0); s.lineTo(0,-len*.16); s.closePath();
  return new THREE.ExtrudeGeometry(s,{depth:.05,bevelEnabled:false});
}
const handMin=new THREE.Mesh(makeHand(5.7,.16), M.brassDark); handMin.position.z=.16; dial.add(handMin);
const handHr=new THREE.Mesh(makeHand(4.0,.22), M.bronze); handHr.position.z=.08; dial.add(handHr);

const bgGears=[];
for(const [x,y,z,r,sp] of [[-6.5,3.2,-10,2.6,.06],[7.2,-2.4,-9.2,1.9,-.09],[2.5,5,-11,3.2,.035]]){
  const g=new THREE.Mesh(makeGear(26,r/13,.16,6), M.brassDark);
  g.position.set(x,y,z); backdrop.add(g); bgGears.push({mesh:g,sp});
}

let dust;
{
  const n=600, pos=new Float32Array(n*3), col=new Float32Array(n*3);
  const cA=new THREE.Color(0xffd9a0), cB=new THREE.Color(0x9fe8dc);
  for(let i=0;i<n;i++){
    const r=4+Math.random()*7, th=Math.random()*Math.PI*2, ph=Math.acos(2*Math.random()-1);
    pos[i*3]=r*Math.sin(ph)*Math.cos(th); pos[i*3+1]=r*Math.cos(ph)*.6; pos[i*3+2]=r*Math.sin(ph)*Math.sin(th);
    const c=Math.random()<.75?cA:cB, k=.35+Math.random()*.65;
    col[i*3]=c.r*k; col[i*3+1]=c.g*k; col[i*3+2]=c.b*k;
  }
  const g=new THREE.BufferGeometry();
  g.setAttribute('position',new THREE.BufferAttribute(pos,3));
  g.setAttribute('color',new THREE.BufferAttribute(col,3));
  dust=new THREE.Points(g,new THREE.PointsMaterial({size:.06,map:spriteTex,transparent:true,
    opacity:.55,vertexColors:true,blending:THREE.AdditiveBlending,depthWrite:false,sizeAttenuation:true}));
  backdrop.add(dust);
}

const POOL=140;
const spPos=new Float32Array(POOL*3), spCol=new Float32Array(POOL*3);
const spVel=[], spLife=new Float32Array(POOL);
for(let i=0;i<POOL;i++){ spPos[i*3+1]=999; spVel.push(new THREE.Vector3()); }
const spGeo=new THREE.BufferGeometry();
spGeo.setAttribute('position',new THREE.BufferAttribute(spPos,3));
spGeo.setAttribute('color',new THREE.BufferAttribute(spCol,3));
const sparks=new THREE.Points(spGeo,new THREE.PointsMaterial({size:.075,map:spriteTex,transparent:true,
  vertexColors:true,blending:THREE.AdditiveBlending,depthWrite:false}));
scene.add(sparks);
const gold=new THREE.Color(1,.72,.35), cyan=new THREE.Color(.55,.95,.88);
function spawnSpark(p){
  for(let i=0;i<POOL;i++){
    if(spLife[i]<=0){
      spLife[i]=1;
      spPos[i*3]=p.x+(Math.random()-.5)*.3; spPos[i*3+1]=p.y+(Math.random()-.5)*.2; spPos[i*3+2]=p.z+(Math.random()-.5)*.3;
      spVel[i].set((Math.random()-.5)*.25,.1+Math.random()*.2,(Math.random()-.5)*.25);
      spVel[i].userData=Math.random()<.85?gold:cyan;
      return;
    }
  }
}

/* ---------- 十一、自研环绕相机 ---------- */
class Orbit {
  constructor(camera, dom){
    this.cam=camera;
    this.target=new THREE.Vector3(0,.45,0);
    this.sph=new THREE.Spherical().setFromVector3(
      new THREE.Vector3().subVectors(camera.position, this.target));
    this.minR=1.4; this.maxR=26; this.minPhi=.12; this.maxPhi=3.0;
    this.autoSpeed=.55; this.autoRotate=false;
    this.fly=null;
    let dragging=false, lx=0, ly=0;
    dom.style.touchAction='none';
    dom.addEventListener('pointerdown', e=>{
      dragging=true; lx=e.clientX; ly=e.clientY;
      this.fly=null;
    });
    addEventListener('pointermove', e=>{
      if(!dragging) return;
      this.sph.theta -= (e.clientX-lx)*.0052;
      this.sph.phi   -= (e.clientY-ly)*.0052;
      lx=e.clientX; ly=e.clientY;
    });
    addEventListener('pointerup',     ()=>dragging=false);
    addEventListener('pointercancel', ()=>dragging=false);
    dom.addEventListener('wheel', e=>{
      e.preventDefault();
      this.sph.radius *= Math.exp(e.deltaY*.0011);
    }, {passive:false});
  }
  flyTo(pos, tgt, dur=2.4){
    const a=new THREE.Spherical().setFromVector3(
      new THREE.Vector3().subVectors(this.cam.position, this.target));
    const b=new THREE.Spherical().setFromVector3(
      new THREE.Vector3(pos[0]-tgt[0], pos[1]-tgt[1], pos[2]-tgt[2]));
    let d=b.theta-a.theta;
    if(d>Math.PI) b.theta-=Math.PI*2; else if(d<-Math.PI) b.theta+=Math.PI*2;
    this.fly={ t:0, dur, a, b, t0:this.target.clone(), t1:new THREE.Vector3(...tgt) };
  }
  update(dt){
    if(this.fly){
      const f=this.fly; f.t+=dt;
      let k=Math.min(1, f.t/f.dur);
      k = k<.5 ? 4*k*k*k : 1-Math.pow(-2*k+2,3)/2;
      this.sph.radius=lerp(f.a.radius, f.b.radius, k);
      this.sph.phi   =lerp(f.a.phi,    f.b.phi,    k);
      this.sph.theta =lerp(f.a.theta,  f.b.theta,  k);
      this.target.lerpVectors(f.t0, f.t1, k);
      if(f.t>=f.dur) this.fly=null;
    }
    if(this.autoRotate) this.sph.theta += this.autoSpeed*dt;
    this.sph.phi    = clamp(this.sph.phi,    this.minPhi, this.maxPhi);
    this.sph.radius = clamp(this.sph.radius, this.minR,   this.maxR);
    this.cam.position.setFromSpherical(this.sph).add(this.target);
    this.cam.lookAt(this.target);
  }
}
const orbit = new Orbit(camera, renderer.domElement);

/* ---------- 十二、自研后期合成(HDR → 辉光 → ACES) ---------- */
class Composer {
  constructor(renderer){
    this.r = renderer;
    const mk=()=> new THREE.WebGLRenderTarget(2,2,{ type:THREE.HalfFloatType, stencilBuffer:false });
    this.rtScene=mk(); this.rtA=mk(); this.rtB=mk();
    this.cam=new THREE.OrthographicCamera(-1,1,1,-1,0,1);
    this.scene=new THREE.Scene();
    this.quad=new THREE.Mesh(new THREE.PlaneGeometry(2,2));
    this.quad.frustumCulled=false;
    this.scene.add(this.quad);
    const VS=`varying vec2 vUv;
      void main(){ vUv=uv; gl_Position=vec4(position.xy,0.0,1.0); }`;
    this.mBright=new THREE.ShaderMaterial({
      uniforms:{ tSrc:{value:null} }, vertexShader:VS, depthTest:false, depthWrite:false,
      fragmentShader:`
        uniform sampler2D tSrc; varying vec2 vUv;
        void main(){
          vec3 c = texture2D(tSrc, vUv).rgb;
          float l = max(max(c.r,c.g),c.b);
          gl_FragColor = vec4(c * smoothstep(0.7, 1.15, l), 1.0);
        }`});
    this.mBlur=new THREE.ShaderMaterial({
      uniforms:{ tSrc:{value:null}, uDir:{value:new THREE.Vector2()} },
      vertexShader:VS, depthTest:false, depthWrite:false,
      fragmentShader:`
        uniform sampler2D tSrc; uniform vec2 uDir; varying vec2 vUv;
        void main(){
          vec3 s = texture2D(tSrc, vUv).rgb * 0.227027;
          s += (texture2D(tSrc, vUv + uDir*1.384615).rgb + texture2D(tSrc, vUv - uDir*1.384615).rgb) * 0.316216;
          s += (texture2D(tSrc, vUv + uDir*3.230769).rgb + texture2D(tSrc, vUv - uDir*3.230769).rgb) * 0.070270;
          gl_FragColor = vec4(s, 1.0);
        }`});
    this.mFinal=new THREE.ShaderMaterial({
      uniforms:{ tScene:{value:null}, tBloom:{value:null},
                 uBloom:{value:.85}, uExposure:{value:1.15} },
      vertexShader:VS, depthTest:false, depthWrite:false,
      fragmentShader:`
        uniform sampler2D tScene, tBloom; uniform float uBloom, uExposure; varying vec2 vUv;
        vec3 aces(vec3 x){
          return clamp( (x*(2.51*x+0.03)) / (x*(2.43*x+0.59)+0.14), 0.0, 1.0 );
        }
        vec3 srgb(vec3 c){
          return mix(c*12.92, pow(c, vec3(0.41666))*1.055-0.055, step(vec3(0.0031308), c));
        }
        void main(){
          vec3 c = texture2D(tScene, vUv).rgb + texture2D(tBloom, vUv).rgb * uBloom;
          gl_FragColor = vec4( srgb( aces(c * uExposure) ), 1.0 );
        }`});
    this._texel=[.5,.5];
  }
  setSize(){
    const v=this.r.getDrawingBufferSize(new THREE.Vector2());
    this.rtScene.setSize(v.x, v.y);
    const w=Math.max(1, v.x>>1), h=Math.max(1, v.y>>1);
    this.rtA.setSize(w,h); this.rtB.setSize(w,h);
    this._texel=[1/w, 1/h];
  }
  _pass(mat, target){
    this.quad.material=mat;
    this.r.setRenderTarget(target);
    this.r.render(this.scene, this.cam);
  }
  render(scene, camera){
    this.r.setRenderTarget(this.rtScene);
    this.r.render(scene, camera);
    this.mBright.uniforms.tSrc.value=this.rtScene.texture;
    this._pass(this.mBright, this.rtA);
    for(let i=0;i<2;i++){
      const k=1+i*1.2;
      this.mBlur.uniforms.tSrc.value=this.rtA.texture;
      this.mBlur.uniforms.uDir.value.set(this._texel[0]*k, 0);
      this._pass(this.mBlur, this.rtB);
      this.mBlur.uniforms.tSrc.value=this.rtB.texture;
      this.mBlur.uniforms.uDir.value.set(0, this._texel[1]*k);
      this._pass(this.mBlur, this.rtA);
    }
    this.mFinal.uniforms.tScene.value=this.rtScene.texture;
    this.mFinal.uniforms.tBloom.value=this.rtA.texture;
    this._pass(this.mFinal, null);
    this.r.setRenderTarget(null);
  }
}
const composer=new Composer(renderer);
composer.setSize();

/* ---------- 十三、交互 ---------- */
const state={ speedMul:1, hover:false, hoverK:0, closeup:false, orbit:false };
const $=id=>document.getElementById(id);
 $('speed').addEventListener('input',e=>{ state.speedMul=e.target.value/100; });
 $('bHover').addEventListener('click',()=>{ state.hover=!state.hover; $('bHover').classList.toggle('on',state.hover); });
 $('bOrbit').addEventListener('click',()=>{
  state.orbit=!state.orbit; state.closeup=false;
  orbit.autoRotate=state.orbit;
  $('bOrbit').classList.toggle('on',state.orbit); $('bClose').classList.remove('on');
  if(state.orbit) orbit.flyTo([6.5,2.6,7],[0,.4,0],2.8);
});
 $('bClose').addEventListener('click',()=>{
  state.closeup=true; state.orbit=false; orbit.autoRotate=false;
  $('bClose').classList.add('on'); $('bOrbit').classList.remove('on');
  const p=butterfly.root.position;
  orbit.flyTo([p.x+1.5,p.y+.7,p.z+1.9],[p.x,p.y,p.z],1.6);
});
 $('bWide').addEventListener('click',()=>{
  state.closeup=false; state.orbit=false; orbit.autoRotate=false;
  $('bClose').classList.remove('on'); $('bOrbit').classList.remove('on');
  orbit.flyTo([4.7,2.1,6.7],[0,.45,0],2.4);
});

/* ---------- 十四、主循环 ---------- */
const CFG={ baseHz:2.3, flapAmp:1.0, rest:.22 };
let t=0, tPath=0, phase=0, yawPrev=0, bank=0;
const clock=new THREE.Clock();

function pathPos(tt,out){
  out.set(
    Math.sin(tt*.23)*2.6+Math.sin(tt*.071)*1.4,
    .75+Math.sin(tt*.31+1.2)*.85+Math.sin(tt*.11)*.45,
    Math.cos(tt*.19+.6)*2.2+Math.sin(tt*.053+2)*1.6
  );
}
function deformWing(w,ph){
  for(const tg of w.targets){
    const pos=tg.geo.attributes.position.array, base=tg.base;
    for(let i=0;i<pos.length;i+=3){
      const x=base[i], y=base[i+1];
      const u=Math.min(1,Math.abs(x)/w.span);
      pos[i+2]=base[i+2]+Math.sin(ph-u*3.2+y*.9)*u*u*w.flex;
    }
    tg.geo.attributes.position.needsUpdate=true;
    if(tg.isMem) tg.geo.computeVertexNormals();
  }
  const im=w.sc.instanceMatrix.array;
  for(let i=0;i<w.scData.length;i++){
    const d=w.scData[i], u=Math.min(1,Math.abs(d.x)/w.span);
    im[i*16+14]=d.bz+Math.sin(ph-u*3.2+d.y*.9)*u*u*w.flex*.9;
  }
  w.sc.instanceMatrix.needsUpdate=true;
}

function tick(){
  const dt=Math.min(clock.getDelta(),.05);
  t+=dt;

  const env=.5+.5*Math.sin(t*.62+Math.sin(t*.23)*1.8);
  const burst=Math.pow(env,3)*1.35;
  state.hoverK=lerp(state.hoverK,state.hover?1:0,1-Math.exp(-dt*2.4));
  const hz=CFG.baseHz*state.speedMul*(1-state.hoverK)*(.72+.5*burst)+.35*state.hoverK+.22;
  const amp=lerp(CFG.flapAmp,.16,state.hoverK);
  phase+=dt*hz*Math.PI*2;

  tPath+=dt*(1-.68*state.hoverK);
  pathPos(tPath,_p0); pathPos(tPath+.05,_p1);
  _dir.copy(_p1).sub(_p0).normalize();
  butterfly.root.position.copy(_p0);
  _m4.lookAt(_dir,_zero,UP);
  _q.setFromRotationMatrix(_m4);
  butterfly.root.quaternion.slerp(_q,1-Math.exp(-dt*3.2));
  const yaw=Math.atan2(_dir.x,_dir.z);
  let yawRate=yaw-yawPrev;
  if(yawRate>Math.PI)yawRate-=Math.PI*2;
  if(yawRate<-Math.PI)yawRate+=Math.PI*2;
  yawPrev=yaw;
  bank=lerp(bank,clamp(-yawRate*2.4,-.55,.55),1-Math.exp(-dt*2));
  butterfly.body.rotation.z=bank+Math.sin(t*1.15)*.03;
  butterfly.body.rotation.x=Math.sin(t*1.3)*.025;

  for(let i=0;i<butterfly.refs.shoulders.length;i++){
    const s=butterfly.refs.shoulders[i], w=butterfly.refs.wings[i];
    const ph=phase-s.off;
    const f=Math.sin(ph)*amp;
    s.roll.rotation.z=s.side*(f+CFG.rest);
    s.tilt.rotation.y=s.side*(-.10-.12*Math.sin(ph+.9));
    s.tilt.rotation.x=.10*Math.sin(ph+2.1);
    deformWing(w,ph);
  }
  for(const g of butterfly.refs.gearsMain) g.mesh.rotation.z=phase*1.1*g.dir;
  for(const g of butterfly.refs.gearsSide) g.mesh.rotation.z=-phase*2.2*g.dir;
  for(const g of butterfly.refs.decoGears) g.rotation.z+=dt*(g.userData.spin||1)*1.4;

  butterfly.refs.abdomen.forEach((seg,i)=>{ seg.rotation.x=Math.sin(t*2.6-i*.55)*.05; });
  butterfly.refs.legs.forEach((leg,i)=>{ leg.rotation.x=Math.sin(t*1.8+i*1.7)*.03; });
  butterfly.head.rotation.y=Math.sin(t*.8)*.08;
  butterfly.core.rotation.y+=dt*.8; butterfly.core.rotation.x+=dt*.3;
  butterfly.cage.rotation.y+=dt*.5;
  const pulse=1+.12*Math.sin(t*2.6);
  butterfly.core.scale.setScalar(pulse);
  butterfly.coreLight.intensity=1.3+.8*Math.sin(t*2.6);

  handMin.rotation.z=-t*.02; handHr.rotation.z=-t*.0017;
  for(const g of bgGears) g.mesh.rotation.z+=dt*g.sp;
  dust.rotation.y+=dt*.006; dust.position.y=Math.sin(t*.1)*.3;

  if(Math.random()<dt*hz*5){
    const tip=butterfly.refs.tips[Math.random()*butterfly.refs.tips.length|0];
    tip.getWorldPosition(_v); spawnSpark(_v);
  }
  for(let i=0;i<POOL;i++){
    if(spLife[i]>0){
      spLife[i]-=dt*.65;
      const k=Math.max(0,spLife[i]);
      spPos[i*3]+=spVel[i].x*dt; spPos[i*3+1]+=spVel[i].y*dt; spPos[i*3+2]+=spVel[i].z*dt;
      const c=spVel[i].userData||gold;
      spCol[i*3]=c.r*k*.85; spCol[i*3+1]=c.g*k*.85; spCol[i*3+2]=c.b*k*.85;
      if(spLife[i]<=0) spPos[i*3+1]=999;
    }
  }
  spGeo.attributes.position.needsUpdate=true;
  spGeo.attributes.color.needsUpdate=true;

  if(state.closeup && !orbit.fly){
    orbit.target.lerp(butterfly.root.position, 1-Math.exp(-dt*3));
  }
  orbit.update(dt);

  if(!tick.hud||t-tick.hud>.12){
    tick.hud=t;
    $('hz').textContent=hz.toFixed(1);
    $('ph').textContent=String(((phase*180/Math.PI)%360+360)%360|0).padStart(3,'0');
    $('st').textContent=state.hoverK>.5?'悬停':(burst>.75?'脉冲':'巡航');
  }

  composer.render(scene, camera);
}

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

/* ---------- 启动 ---------- */
orbit.flyTo([4.7,2.1,6.7],[0,.45,0],4.5);
window.__ok = true;
document.getElementById('err').style.display='none';
requestAnimationFrame(()=>document.body.classList.add('ready'));
renderer.setAnimationLoop(tick);

} /* main 结束 */
</script>
</body>
</html>

二、Qwen3.8-Max:产品经理转世,工程卷王

2738 行代码,113KB,是三个文件里最重的一个。但它不是重在堆模型,而是重在做产品:

  • 完整控制台:3 种飞行模式、振翅频率、辉光强度实时可调;
  • 3 套材质主题(黄铜古董 / 青金机械 / 玫瑰琉璃),切换时所有颜色逐帧插值过渡;
  • 结构透视、爆炸拆解视图、部件标注热点+点击弹出信息卡;
  • 键盘快捷键、FPS/面数/DrawCall 实时统计、带进度条的加载页;
  • BEM 命名规范、ARIA 无障碍、响应式适配、连 prefers-reduced-motion 都照顾到了。

一句话总结:别人交作品,它交的是一个可以上线的小型数字展馆。

以下为Qwen-3.8Max生成的机械蝴蝶代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" />
<meta name="theme-color" content="#05080c" />
<title>机械蝴蝶 · Clockwork Lepidoptera</title>
<style>
/* SECTION: base */
*,*::before,*::after{box-sizing:border-box}
:root{
  --bg:#05080c;
  --accent:#ffb347;
  --accent-2:#6ff2ff;
  --ink:#e9eef4;
  --ink-dim:#93a1b0;
  --glass:rgba(12,18,26,.62);
  --glass-brd:rgba(255,255,255,.10);
  --shadow:0 18px 48px rgba(0,0,0,.55);
  --r:14px;
  --panel-w:288px;
  --tr:.42s cubic-bezier(.4,0,.2,1);
}
html,body{height:100%}
body{
  margin:0;background:var(--bg);color:var(--ink);
  font-family:"PingFang SC","Hiragino Sans GB","Microsoft YaHei","Segoe UI",system-ui,-apple-system,sans-serif;
  overflow:hidden;-webkit-font-smoothing:antialiased;
  -webkit-tap-highlight-color:transparent;
}
button,input,select{font:inherit;color:inherit}
kbd{
  display:inline-block;padding:1px 6px;border-radius:5px;font-size:11px;line-height:1.6;
  background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.16);
  border-bottom-width:2px;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;
}
/* SECTION: stage */
#stage{position:fixed;inset:0}
#scene{display:block;width:100%;height:100%;touch-action:none;cursor:grab}
#scene.is-drag{cursor:grabbing}
/* SECTION: hud */
#hud{position:fixed;inset:0;pointer-events:none;z-index:5}
#hud>*{pointer-events:auto}
.brand{
  position:absolute;left:26px;top:22px;display:flex;gap:13px;align-items:center;
  pointer-events:none;user-select:none;
}
.brand__mark{
  width:38px;height:38px;border-radius:50%;flex:0 0 auto;position:relative;
  background:
    radial-gradient(circle at 50% 50%,transparent 0 27%,var(--accent) 27% 33%,transparent 33%),
    conic-gradient(from 0deg,var(--accent),var(--accent-2),var(--accent));
  -webkit-mask:radial-gradient(circle,#000 0 100%);
  mask:radial-gradient(circle,#000 0 100%);
  filter:drop-shadow(0 0 10px color-mix(in srgb,var(--accent) 55%,transparent));
  animation:spin 9s linear infinite;transition:filter var(--tr);
}
.brand__mark::after{
  content:"";position:absolute;inset:11px;border-radius:50%;
  border:1.5px dashed rgba(255,255,255,.55);
}
@keyframes spin{to{transform:rotate(360deg)}}
.brand__text h1{
  margin:0;font-size:23px;font-weight:650;letter-spacing:.16em;line-height:1.15;
  text-shadow:0 2px 18px rgba(0,0,0,.8);
}
.brand__text p{
  margin:4px 0 0;font-size:10.5px;letter-spacing:.19em;color:var(--ink-dim);
  text-transform:uppercase;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;
}
/* SECTION: panel */
.panel{
  position:absolute;right:22px;top:22px;width:var(--panel-w);max-height:calc(100dvh - 44px);
  display:flex;flex-direction:column;
  background:var(--glass);border:1px solid var(--glass-brd);border-radius:var(--r);
  box-shadow:var(--shadow);backdrop-filter:blur(20px) saturate(140%);
  -webkit-backdrop-filter:blur(20px) saturate(140%);overflow:hidden;
}
.panel__toggle{
  display:none;width:100%;align-items:center;justify-content:space-between;gap:8px;
  padding:13px 15px;background:none;border:0;cursor:pointer;
}
.panel__title{font-size:13px;letter-spacing:.16em;color:var(--ink-dim)}
.panel__chev{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;opacity:.7}
.panel__body{overflow-y:auto;overscroll-behavior:contain;padding:4px 15px 15px;scrollbar-width:thin}
.panel__body::-webkit-scrollbar{width:5px}
.panel__body::-webkit-scrollbar-thumb{background:rgba(255,255,255,.16);border-radius:3px}
.ctl{padding:13px 0;border-top:1px solid rgba(255,255,255,.07)}
.ctl:first-child{border-top:0}
.ctl h2{
  margin:0 0 10px;font-size:11px;font-weight:600;letter-spacing:.14em;color:var(--ink-dim);
  display:flex;align-items:center;justify-content:space-between;gap:8px;text-transform:uppercase;
}
.ctl h2 output{
  font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11.5px;
  color:var(--accent);letter-spacing:.04em;transition:color var(--tr);
}
.seg{display:grid;grid-template-columns:repeat(3,1fr);gap:6px}
.seg__btn{
  padding:9px 4px;border-radius:9px;cursor:pointer;font-size:12.5px;letter-spacing:.04em;
  background:rgba(255,255,255,.045);border:1px solid rgba(255,255,255,.09);
  color:var(--ink-dim);transition:all .22s ease;
}
.seg__btn:hover{background:rgba(255,255,255,.09);color:var(--ink)}
.seg__btn.is-on{
  background:color-mix(in srgb,var(--accent) 20%,transparent);
  border-color:color-mix(in srgb,var(--accent) 55%,transparent);
  color:var(--accent);box-shadow:inset 0 0 14px color-mix(in srgb,var(--accent) 16%,transparent);
}
input[type=range]{
  -webkit-appearance:none;appearance:none;width:100%;height:22px;background:none;cursor:pointer;
}
input[type=range]::-webkit-slider-runnable-track{
  height:4px;border-radius:3px;
  background:linear-gradient(90deg,var(--accent) 0 var(--fill,50%),rgba(255,255,255,.13) var(--fill,50%) 100%);
}
input[type=range]::-webkit-slider-thumb{
  -webkit-appearance:none;width:15px;height:15px;border-radius:50%;margin-top:-5.5px;
  background:#fff;border:2.5px solid var(--accent);
  box-shadow:0 0 10px color-mix(in srgb,var(--accent) 70%,transparent);
}
input[type=range]::-moz-range-track{height:4px;border-radius:3px;background:rgba(255,255,255,.13)}
input[type=range]::-moz-range-progress{height:4px;border-radius:3px;background:var(--accent)}
input[type=range]::-moz-range-thumb{
  width:13px;height:13px;border-radius:50%;background:#fff;border:2.5px solid var(--accent);
}
.themes{display:grid;grid-template-columns:repeat(3,1fr);gap:6px}
.theme{
  display:flex;flex-direction:column;align-items:center;gap:6px;padding:9px 3px;border-radius:9px;
  cursor:pointer;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.09);
  transition:all .22s ease;
}
.theme i{
  width:22px;height:22px;border-radius:50%;display:block;
  background:linear-gradient(135deg,var(--c1),var(--c2));
  box-shadow:0 0 12px color-mix(in srgb,var(--c2) 60%,transparent);
}
.theme span{font-size:10.5px;color:var(--ink-dim);letter-spacing:.02em}
.theme:hover{background:rgba(255,255,255,.08)}
.theme.is-on{border-color:color-mix(in srgb,var(--accent) 55%,transparent);background:color-mix(in srgb,var(--accent) 12%,transparent)}
.theme.is-on span{color:var(--ink)}
.toggles{display:grid;grid-template-columns:1fr 1fr;gap:6px}
.tgl{
  padding:8px 6px;border-radius:9px;cursor:pointer;font-size:12px;
  background:rgba(255,255,255,.045);border:1px solid rgba(255,255,255,.09);
  color:var(--ink-dim);transition:all .22s ease;position:relative;
}
.tgl:hover{background:rgba(255,255,255,.09);color:var(--ink)}
.tgl.is-on{
  color:var(--accent-2);border-color:color-mix(in srgb,var(--accent-2) 48%,transparent);
  background:color-mix(in srgb,var(--accent-2) 13%,transparent);
}
.btn{
  width:100%;padding:10px;border-radius:9px;cursor:pointer;font-size:13px;letter-spacing:.08em;
  background:linear-gradient(180deg,rgba(255,255,255,.11),rgba(255,255,255,.04));
  border:1px solid rgba(255,255,255,.14);transition:all .2s ease;
}
.btn:hover{border-color:color-mix(in srgb,var(--accent) 55%,transparent);color:var(--accent)}
.btn:active{transform:translateY(1px)}
.keys{margin:10px 0 0;font-size:10.5px;line-height:2;color:var(--ink-dim)}
/* SECTION: hotspots */
#hotspots{position:absolute;inset:0;pointer-events:none}
.hs{position:absolute;left:0;top:0;will-change:transform;pointer-events:auto}
.hs__dot{
  width:22px;height:22px;margin:-11px 0 0 -11px;border-radius:50%;cursor:pointer;
  background:none;border:0;padding:0;position:relative;display:block;
}
.hs__dot::before{
  content:"";position:absolute;inset:7px;border-radius:50%;background:var(--accent);
  box-shadow:0 0 12px var(--accent);transition:inset .2s ease;
}
.hs__dot::after{
  content:"";position:absolute;inset:0;border-radius:50%;
  border:1px solid color-mix(in srgb,var(--accent) 65%,transparent);
  animation:ping 2.6s cubic-bezier(0,0,.2,1) infinite;
}
@keyframes ping{0%{transform:scale(.55);opacity:.9}70%,100%{transform:scale(1.5);opacity:0}}
.hs__dot:hover::before{inset:5px}
.hs.is-sel .hs__dot::before{inset:4px;background:var(--accent-2);box-shadow:0 0 16px var(--accent-2)}
.hs__label{
  position:absolute;left:16px;top:-9px;white-space:nowrap;padding:3px 9px;border-radius:7px;
  font-size:11px;letter-spacing:.05em;color:var(--ink);
  background:rgba(8,12,18,.78);border:1px solid rgba(255,255,255,.12);
  opacity:0;transform:translateX(-4px);transition:all .2s ease;pointer-events:none;
}
.hs:hover .hs__label,.hs.is-sel .hs__label{opacity:1;transform:none}
/* SECTION: infocard */
.infocard{
  position:absolute;left:26px;bottom:26px;width:318px;max-width:calc(100vw - 52px);
  background:var(--glass);border:1px solid var(--glass-brd);border-radius:var(--r);
  box-shadow:var(--shadow);backdrop-filter:blur(20px) saturate(140%);
  -webkit-backdrop-filter:blur(20px) saturate(140%);
  padding:16px 17px 15px;animation:rise .34s cubic-bezier(.2,.9,.3,1);
}
.infocard[hidden]{display:none}
@keyframes rise{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
.infocard__k{
  font-size:10px;letter-spacing:.2em;color:var(--accent);text-transform:uppercase;
  font-family:ui-monospace,SFMono-Regular,Consolas,monospace;
}
.infocard h3{margin:6px 0 0;font-size:16.5px;font-weight:620;letter-spacing:.05em}
.infocard p{margin:9px 0 0;font-size:12.5px;line-height:1.85;color:var(--ink-dim)}
.infocard__x{
  position:absolute;right:9px;top:8px;width:26px;height:26px;border-radius:7px;cursor:pointer;
  background:none;border:0;color:var(--ink-dim);font-size:17px;line-height:1;
}
.infocard__x:hover{background:rgba(255,255,255,.09);color:var(--ink)}
/* SECTION: stats + hint */
.stats{
  position:absolute;right:24px;bottom:22px;text-align:right;pointer-events:none;
  font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10.5px;
  letter-spacing:.06em;color:rgba(147,161,176,.72);line-height:1.85;
}
.stats b{color:var(--accent-2);font-weight:500;transition:color var(--tr)}
.hint{
  position:absolute;left:50%;bottom:26px;transform:translateX(-50%);
  padding:7px 16px;border-radius:99px;font-size:11.5px;letter-spacing:.07em;color:var(--ink-dim);
  background:rgba(8,12,18,.6);border:1px solid rgba(255,255,255,.09);
  backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);
  transition:opacity .8s ease;white-space:nowrap;pointer-events:none;
}
.hint.is-gone{opacity:0}
/* SECTION: loader */
#loader{
  position:fixed;inset:0;z-index:40;display:grid;place-items:center;
  background:radial-gradient(ellipse at 50% 42%,#0d1620 0%,#05080c 62%,#020406 100%);
  transition:opacity .7s ease,visibility .7s ease;
}
#loader.is-done{opacity:0;visibility:hidden;pointer-events:none}
.ld{display:flex;flex-direction:column;align-items:center;gap:22px;padding:0 28px;text-align:center}
.ld__gear{width:104px;height:104px;position:relative}
.ld__gear svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}
.ld__g1{animation:spin 6s linear infinite;transform-origin:50% 50%}
.ld__g2{animation:spin 4s linear infinite reverse;transform-origin:50% 50%}
.ld__ring{fill:none;stroke:var(--accent);stroke-width:5;stroke-dasharray:5.5 9;stroke-linecap:round;opacity:.92}
.ld__ring2{fill:none;stroke:var(--accent-2);stroke-width:3;stroke-dasharray:3 7;opacity:.55}
.ld__core{fill:none;stroke:rgba(255,255,255,.28);stroke-width:1.4}
.ld__core2{fill:var(--accent);opacity:.85}
.ld h2{margin:0;font-size:19px;letter-spacing:.34em;font-weight:500;text-indent:.34em}
.ld__sub{margin:-14px 0 0;font-size:10px;letter-spacing:.24em;color:var(--ink-dim);text-transform:uppercase}
.ld__bar{width:min(280px,72vw);height:2px;background:rgba(255,255,255,.11);border-radius:2px;overflow:hidden}
.ld__fill{height:100%;width:0%;background:linear-gradient(90deg,var(--accent),var(--accent-2));transition:width .3s ease;box-shadow:0 0 12px var(--accent)}
.ld__msg{font-size:11.5px;letter-spacing:.1em;color:var(--ink-dim);min-height:1.4em}
.ld__err{
  max-width:400px;font-size:12.5px;line-height:1.9;color:#ffb4a2;
  background:rgba(120,30,20,.16);border:1px solid rgba(255,120,90,.28);
  border-radius:11px;padding:13px 16px;display:none;
}
.ld.is-err .ld__err{display:block}
.ld.is-err .ld__bar,.ld.is-err .ld__msg{display:none}
/* SECTION: responsive */
@media (max-width:900px){
  :root{--panel-w:auto}
  .brand{left:16px;top:14px;gap:10px}
  .brand__mark{width:30px;height:30px}
  .brand__text h1{font-size:17px;letter-spacing:.12em}
  .brand__text p{font-size:8.5px;letter-spacing:.13em;margin-top:3px}
  .panel{
    left:12px;right:12px;top:auto;bottom:12px;width:auto;max-height:56dvh;
  }
  .panel__toggle{display:flex}
  .panel__body{padding:0 14px 14px}
  .panel.is-collapsed .panel__body{display:none}
  .panel.is-collapsed{max-height:none}
  .infocard{left:12px;right:12px;bottom:auto;top:74px;width:auto;max-width:none}
  .stats{right:14px;bottom:auto;top:14px;font-size:9.5px}
  .hint{display:none}
  .hs__label{display:none}
  .seg,.themes{grid-template-columns:repeat(3,1fr)}
  .toggles{grid-template-columns:1fr 1fr}
}
@media (max-width:900px) and (max-height:520px){
  .infocard{top:auto;bottom:calc(56dvh + 8px)}
}
@media (prefers-reduced-motion:reduce){
  .brand__mark,.ld__g1,.ld__g2{animation:none}
}
</style>
<script type="importmap">
{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"}}
</script>
</head>
<body>
<!-- SECTION: stage -->
<div id="stage"><canvas id="scene"></canvas></div>

<!-- SECTION: hud -->
<div id="hud">
  <header class="brand">
    <div class="brand__mark" aria-hidden="true"></div>
    <div class="brand__text">
      <h1>机械蝴蝶</h1>
      <p>Clockwork Lepidoptera · Three.js</p>
    </div>
  </header>

  <div id="hotspots"></div>

  <aside id="panel" class="panel is-collapsed" aria-label="场景控制台">
    <button id="panelToggle" class="panel__toggle" aria-expanded="false" aria-controls="panelBody">
      <span class="panel__title">控制台 · CONSOLE</span>
      <svg class="panel__chev" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6" stroke-linecap="round" stroke-linejoin="round"/></svg>
    </button>
    <div id="panelBody" class="panel__body">
      <section class="ctl">
        <h2>飞行模式</h2>
        <div class="seg" id="modeSeg" role="group" aria-label="飞行模式">
          <button type="button" class="seg__btn is-on" data-mode="hover">悬停</button>
          <button type="button" class="seg__btn" data-mode="orbit" data-primary-action>环绕飞行</button>
          <button type="button" class="seg__btn" data-mode="display">展翼陈列</button>
        </div>
      </section>
      <section class="ctl">
        <h2>振翅频率 <output id="freqOut" for="freq">1.00×</output></h2>
        <input id="freq" type="range" min="0.2" max="2.4" step="0.05" value="1" aria-label="振翅频率" />
      </section>
      <section class="ctl">
        <h2>辉光强度 <output id="bloomOut" for="bloomS">0.85</output></h2>
        <input id="bloomS" type="range" min="0" max="2" step="0.05" value="0.85" aria-label="辉光强度" />
      </section>
      <section class="ctl">
        <h2>材质主题</h2>
        <div class="themes" id="themes" role="group" aria-label="材质主题">
          <button type="button" class="theme is-on" data-theme="brass"><i style="--c1:#c08a3e;--c2:#ffb04a"></i><span>黄铜古董</span></button>
          <button type="button" class="theme" data-theme="steel"><i style="--c1:#5f7f96;--c2:#6ff2ff"></i><span>青金机械</span></button>
          <button type="button" class="theme" data-theme="rose"><i style="--c1:#c07a86;--c2:#ff7ac6"></i><span>玫瑰琉璃</span></button>
        </div>
      </section>
      <section class="ctl">
        <h2>视图</h2>
        <div class="toggles">
          <button type="button" class="tgl is-on" id="tSpin" aria-pressed="true">自动环绕</button>
          <button type="button" class="tgl" id="tXray" aria-pressed="false">结构透视</button>
          <button type="button" class="tgl" id="tExplode" aria-pressed="false">拆解视图</button>
          <button type="button" class="tgl is-on" id="tHot" aria-pressed="true">部件标注</button>
        </div>
      </section>
      <section class="ctl">
        <button type="button" id="reset" class="btn">复位视角</button>
        <p class="keys"><kbd>空格</kbd> 暂停 · <kbd>W</kbd> 透视 · <kbd>E</kbd> 拆解 · <kbd>R</kbd> 复位</p>
      </section>
    </div>
  </aside>

  <div id="infocard" class="infocard" hidden role="dialog" aria-live="polite">
    <button type="button" class="infocard__x" id="infoX" aria-label="关闭">×</button>
    <div class="infocard__k" id="infoK">COMPONENT</div>
    <h3 id="infoT">部件</h3>
    <p id="infoD">说明</p>
  </div>

  <div id="stats" class="stats" aria-hidden="true">
    <div><b id="sFps">--</b> FPS</div>
    <div><b id="sTri">--</b> TRIS</div>
    <div><b id="sCall">--</b> CALLS</div>
  </div>

  <div id="hint" class="hint">拖拽旋转视角 · 滚轮或双指缩放 · 点击光点查看部件</div>
</div>

<!-- SECTION: loader -->
<div id="loader">
  <div class="ld" id="ld">
    <div class="ld__gear" aria-hidden="true">
      <svg viewBox="0 0 100 100">
        <g class="ld__g1"><circle class="ld__ring" cx="50" cy="50" r="42"/></g>
        <g class="ld__g2"><circle class="ld__ring2" cx="50" cy="50" r="29"/></g>
        <circle class="ld__core" cx="50" cy="50" r="17"/>
        <circle class="ld__core2" cx="50" cy="50" r="4.5"/>
      </svg>
    </div>
    <h2>机械蝴蝶</h2>
    <p class="ld__sub">Clockwork Lepidoptera</p>
    <div class="ld__bar"><div class="ld__fill" id="ldFill"></div></div>
    <div class="ld__msg" id="ldMsg">正在初始化渲染器…</div>
    <div class="ld__err" id="ldErr"></div>
  </div>
</div>

<script type="module">
/* =====================================================================
   机械蝴蝶 · Clockwork Lepidoptera
   全部几何体、贴图、材质均在运行时程序化生成,不加载任何外部模型或图片。
   唯一外部依赖:three@0.160.0(ES Module,经 importmap 引入)。
   ===================================================================== */

/* SECTION: boot */
const $ = (id) => document.getElementById(id);
const loaderEl = $('loader'), ldEl = $('ld'), ldFill = $('ldFill'), ldMsg = $('ldMsg'), ldErr = $('ldErr');
const TOTAL_STEPS = 12;
let loadStep = 0;
function step(msg) {
  loadStep++;
  ldFill.style.width = Math.min(100, (loadStep / TOTAL_STEPS) * 100).toFixed(1) + '%';
  ldMsg.textContent = msg;
}
function fatal(title, detail) {
  ldEl.classList.add('is-err');
  ldErr.textContent = '';
  const b = document.createElement('strong');
  b.textContent = title;
  ldErr.appendChild(b);
  ldErr.appendChild(document.createElement('br'));
  ldErr.appendChild(document.createTextNode(detail || ''));
}

let THREE, EffectComposer, RenderPass, UnrealBloomPass, OutputPass, mergeGeometries;
step('正在加载 Three.js 引擎…');
try {
  THREE = await import('three');
  ({ EffectComposer } = await import('three/addons/postprocessing/EffectComposer.js'));
  ({ RenderPass } = await import('three/addons/postprocessing/RenderPass.js'));
  ({ UnrealBloomPass } = await import('three/addons/postprocessing/UnrealBloomPass.js'));
  ({ OutputPass } = await import('three/addons/postprocessing/OutputPass.js'));
  ({ mergeGeometries } = await import('three/addons/utils/BufferGeometryUtils.js'));
} catch (err) {
  fatal('渲染引擎加载失败',
    String((err && err.message) || err) +
    '。本页通过公共 CDN 以 ES Module 引入 three@0.160.0,请确认网络可访问 cdn.jsdelivr.net 后刷新重试。');
  throw err;
}
const {
  Vector2, Vector3, Color, Group, Mesh, Object3D, Scene, PerspectiveCamera, WebGLRenderer,
  SphereGeometry, BoxGeometry, CylinderGeometry, TorusGeometry, ConeGeometry, RingGeometry,
  CircleGeometry, PlaneGeometry, LatheGeometry, TubeGeometry, ShapeGeometry, ExtrudeGeometry,
  InstancedMesh, BufferGeometry, BufferAttribute, Float32BufferAttribute, Points, PointsMaterial,
  Shape, Path, CatmullRomCurve3, Curve, Matrix4, Quaternion, Euler, Spherical, Raycaster, Clock,
  MeshStandardMaterial, MeshPhysicalMaterial, MeshBasicMaterial, ShaderMaterial, SpriteMaterial, Sprite,
  DirectionalLight, SpotLight, PointLight, AmbientLight, HemisphereLight,
  PMREMGenerator, DataTexture, CanvasTexture, RepeatWrapping, MirroredRepeatWrapping,
  ACESFilmicToneMapping, PCFSoftShadowMap, SRGBColorSpace, AdditiveBlending, DoubleSide, BackSide,
  FrontSide, NormalBlending, MathUtils
} = THREE;

/* SECTION: themes */
const THEMES = {
  brass: {
    css: ['#ffb347', '#6ff2ff'],
    accent: 0xffb347, accent2: 0x6ff2ff,
    metal: 0xb5813a, metal2: 0x8d6a3f, metal3: 0x6d5433,
    dark: 0x221c14, shell: 0x2e2418,
    copper: 0xb4663c,
    core: 0xffab3a, corePower: 3.2,
    membrane: 0xff9c3a, membraneEmis: 1.05, wingTint: 0xffc98a,
    bgTop: 0x15100a, bgBot: 0x04060a, bgGlow: 0x3a2410, fog: 0x0a0c11,
    key: 0xffd9a8, keyI: 2.5, rim: 0x5fd4ff, rimI: 2.1, fill: 0x33465f, fillI: 0.55,
    hemiSky: 0x4a5f7a, hemiGnd: 0x1a130c, envI: 1.15,
    plate: 0x1a1f28, floor: 0x0c1016
  },
  steel: {
    css: ['#6ff2ff', '#7aa8ff'],
    accent: 0x6ff2ff, accent2: 0x8fb6ff,
    metal: 0x93a8b6, metal2: 0x6f8794, metal3: 0x4e626d,
    dark: 0x141a1f, shell: 0x1c2429,
    copper: 0x5f7f8c,
    core: 0x74e6ff, corePower: 3.6,
    membrane: 0x4fc8ff, membraneEmis: 1.15, wingTint: 0xa9e8ff,
    bgTop: 0x0b141c, bgBot: 0x03060a, bgGlow: 0x12384a, fog: 0x070c11,
    key: 0xd6f2ff, keyI: 2.3, rim: 0x7aa8ff, rimI: 2.4, fill: 0x26404f, fillI: 0.6,
    hemiSky: 0x3f6478, hemiGnd: 0x0c1216, envI: 1.35,
    plate: 0x151c24, floor: 0x0a0f14
  },
  rose: {
    css: ['#ff7ac6', '#ffc46b'],
    accent: 0xff7ac6, accent2: 0xffc46b,
    metal: 0xc08a92, metal2: 0x9c7079, metal3: 0x77565d,
    dark: 0x1d1418, shell: 0x2a1c22,
    copper: 0xd08a7a,
    core: 0xff6fc0, corePower: 3.4,
    membrane: 0xff78c8, membraneEmis: 1.1, wingTint: 0xffc2e2,
    bgTop: 0x170c14, bgBot: 0x06040a, bgGlow: 0x46143a, fog: 0x0d0911,
    key: 0xffd6ee, keyI: 2.4, rim: 0xffc46b, rimI: 2.2, fill: 0x4a2f45, fillI: 0.6,
    hemiSky: 0x6a4460, hemiGnd: 0x160f13, envI: 1.2,
    plate: 0x201821, floor: 0x100b10
  }
};
const themeKeys = Object.keys(THEMES);
let themeKey = 'brass';
const T = () => THEMES[themeKey];

/* SECTION: renderer */
const canvas = $('scene');
const renderer = new WebGLRenderer({ canvas, antialias: true, powerPreference: 'high-performance' });
const isTouch = matchMedia('(hover:none)').matches;
let pixelRatio = Math.min(window.devicePixelRatio || 1, isTouch ? 1.7 : 2);
renderer.setPixelRatio(pixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight, false);
renderer.toneMapping = ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.06;
renderer.outputColorSpace = SRGBColorSpace;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = PCFSoftShadowMap;

const scene = new Scene();
scene.fog = new THREE.FogExp2(T().fog, 0.0165);

const camera = new PerspectiveCamera(42, window.innerWidth / window.innerHeight, 0.1, 500);

/* SECTION: camera-rig */
const camState = {
  target: new Vector3(0, 2.35, 0),
  targetGoal: new Vector3(0, 2.35, 0),
  sph: new Spherical(12.4, 1.24, 0.62),
  goal: new Spherical(12.4, 1.24, 0.62),
  home: { r: 12.4, phi: 1.24, theta: 0.62, tx: 0, ty: 2.35, tz: 0 },
  dragging: false, autoRotate: true, shake: 0,
  pointers: new Map(), pinchStart: 0, pinchR: 0, moved: 0,
  reset() {
    const h = this.home;
    this.goal.set(h.r, h.phi, h.theta);
    this.targetGoal.set(h.tx, h.ty, h.tz);
  },
  update(dt) {
    if (this.autoRotate && !this.dragging) this.goal.theta += dt * 0.135;
    const k = 1 - Math.pow(0.0016, dt);
    this.sph.radius += (this.goal.radius - this.sph.radius) * k;
    this.sph.phi += (this.goal.phi - this.sph.phi) * k;
    this.sph.theta += (this.goal.theta - this.sph.theta) * k;
    this.target.lerp(this.targetGoal, k);
    this.sphi_makeSafe();
    camera.position.setFromSpherical(this.sph).add(this.target);
    if (this.shake > 0.0005) {
      this.shake *= Math.pow(0.02, dt);
      camera.position.x += (Math.random() - 0.5) * this.shake;
      camera.position.y += (Math.random() - 0.5) * this.shake;
    }
    camera.lookAt(this.target);
  },
  sphi_makeSafe() {
    this.sph.phi = Math.min(Math.max(this.sph.phi, 0.14), Math.PI - 0.14);
    this.sph.makeSafe && this.sph.makeSafe();
  }
};

const pointers = camState.pointers;
canvas.addEventListener('pointerdown', (e) => {
  canvas.setPointerCapture(e.pointerId);
  pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
  if (pointers.size === 1) { camState.dragging = true; canvas.classList.add('is-drag'); }
  if (pointers.size === 2) {
    const p = [...pointers.values()];
    camState.pinchStart = Math.hypot(p[0].x - p[1].x, p[0].y - p[1].y);
    camState.pinchR = camState.goal.radius;
  }
  camState.moved = 0;
});
canvas.addEventListener('pointermove', (e) => {
  const prev = pointers.get(e.pointerId);
  if (!prev) return;
  const dx = e.clientX - prev.x, dy = e.clientY - prev.y;
  prev.x = e.clientX; prev.y = e.clientY;
  camState.moved += Math.abs(dx) + Math.abs(dy);
  if (pointers.size >= 2) {
    const p = [...pointers.values()];
    const d = Math.hypot(p[0].x - p[1].x, p[0].y - p[1].y);
    if (camState.pinchStart > 4) {
      camState.goal.radius = MathUtils.clamp(camState.pinchR * (camState.pinchStart / d), 5.2, 30);
    }
    return;
  }
  camState.goal.theta -= dx * 0.0058;
  camState.goal.phi = MathUtils.clamp(camState.goal.phi - dy * 0.0052, 0.16, Math.PI - 0.16);
});
function endPointer(e) {
  pointers.delete(e.pointerId);
  if (pointers.size < 2) camState.pinchStart = 0;
  if (pointers.size === 0) { camState.dragging = false; canvas.classList.remove('is-drag'); }
}
canvas.addEventListener('pointerup', endPointer);
canvas.addEventListener('pointercancel', endPointer);
canvas.addEventListener('lostpointercapture', endPointer);
canvas.addEventListener('wheel', (e) => {
  e.preventDefault();
  camState.goal.radius = MathUtils.clamp(camState.goal.radius * (1 + Math.sign(e.deltaY) * Math.min(0.22, Math.abs(e.deltaY) * 0.0012)), 5.2, 30);
}, { passive: false });

/* SECTION: background */
const bgUniforms = {
  cTop: { value: new Color(T().bgTop) },
  cBot: { value: new Color(T().bgBot) },
  cGlow: { value: new Color(T().bgGlow) },
  uTime: { value: 0 }
};
const bgMesh = new Mesh(new SphereGeometry(180, 40, 28), new ShaderMaterial({
  side: BackSide, depthWrite: false, fog: false, uniforms: bgUniforms,
  vertexShader: `
    varying vec3 vDir;
    void main(){ vDir = normalize(position); gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }`,
  fragmentShader: `
    uniform vec3 cTop, cBot, cGlow; uniform float uTime; varying vec3 vDir;
    float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7))) * 43758.5453); }
    float noise(vec2 p){
      vec2 i = floor(p), f = fract(p); f = f*f*(3.0-2.0*f);
      return mix(mix(hash(i), hash(i+vec2(1,0)), f.x), mix(hash(i+vec2(0,1)), hash(i+vec2(1,1)), f.x), f.y);
    }
    void main(){
      vec3 d = normalize(vDir);
      float h = d.y * 0.5 + 0.5;
      vec3 c = mix(cBot, cTop, pow(clamp(h,0.0,1.0), 1.35));
      float glow = pow(max(0.0, 1.0 - length(vec2(d.x, d.y * 1.7 - 0.15))), 3.0);
      c += cGlow * glow * 0.75;
      float n = noise(d.xz * 3.2 + vec2(uTime*0.012, -uTime*0.008));
      n += noise(d.xy * 7.5 - uTime*0.006) * 0.45;
      c += cGlow * smoothstep(0.72, 1.35, n) * 0.16;
      float st = step(0.9985, hash(floor(d.xz * 260.0 + d.y * 140.0)));
      c += vec3(st) * 0.55;
      c *= 1.0 - 0.42 * pow(max(0.0, -d.y), 2.0);
      gl_FragColor = vec4(c, 1.0);
    }`
}));
bgMesh.frustumCulled = false;
bgMesh.renderOrder = -1000;
scene.add(bgMesh);

/* SECTION: environment */
step('正在生成金属反射环境…');
function buildEnvScene() {
  const s = new Scene();
  const room = new Mesh(new BoxGeometry(26, 18, 26), new MeshBasicMaterial({ side: BackSide, color: 0x090d13 }));
  s.add(room);
  const panel = (color, inten, pos, scl, rot) => {
    const m = new Mesh(new PlaneGeometry(1, 1), new MeshBasicMaterial({ color: new Color(color).multiplyScalar(inten), side: DoubleSide }));
    m.position.set(pos[0], pos[1], pos[2]);
    m.scale.set(scl[0], scl[1], 1);
    if (rot) m.rotation.set(rot[0], rot[1], rot[2]);
    s.add(m);
  };
  panel(0xfff0d8, 5.2, [0, 8.4, 1.5], [13, 9], [Math.PI / 2, 0, 0]);
  panel(0x9fd8ff, 3.1, [-9.5, 2.4, -2], [11, 8], [0, Math.PI / 2, 0]);
  panel(0xffb060, 2.2, [9.5, 1.2, -3], [10, 7], [0, -Math.PI / 2, 0]);
  panel(0x2a3644, 1.0, [0, -3, -9], [16, 9], [0, 0, 0]);
  panel(0xffffff, 1.4, [2, 5, 8.6], [6, 4], [0, Math.PI, 0]);
  return s;
}
const pmrem = new PMREMGenerator(renderer);
pmrem.compileEquirectangularShader();
const envRT = pmrem.fromScene(buildEnvScene(), 0.035);
scene.environment = envRT.texture;

/* SECTION: lights */
const keyLight = new DirectionalLight(T().key, T().keyI);
keyLight.position.set(6.5, 11, 7);
keyLight.castShadow = true;
keyLight.shadow.mapSize.set(2048, 2048);
keyLight.shadow.camera.near = 1;
keyLight.shadow.camera.far = 42;
keyLight.shadow.camera.left = -11;
keyLight.shadow.camera.right = 11;
keyLight.shadow.camera.top = 11;
keyLight.shadow.camera.bottom = -11;
keyLight.shadow.bias = -0.0009;
keyLight.shadow.normalBias = 0.028;
keyLight.shadow.radius = 2.2;
scene.add(keyLight, keyLight.target);
keyLight.target.position.set(0, 1.6, 0);

const rimLight = new DirectionalLight(T().rim, T().rimI);
rimLight.position.set(-8, 5.5, -9);
scene.add(rimLight);

const fillLight = new DirectionalLight(T().fill, T().fillI);
fillLight.position.set(-5, -3, 6);
scene.add(fillLight);

const hemi = new HemisphereLight(T().hemiSky, T().hemiGnd, 0.42);
scene.add(hemi);

const coreLight = new PointLight(T().core, T().corePower, 9, 2);
coreLight.position.set(0, 0, 0);

const wingLightL = new PointLight(T().accent, 1.5, 7, 2);
const wingLightR = new PointLight(T().accent, 1.5, 7, 2);

/* SECTION: textures — 全部由 Canvas 2D 实时绘制,无任何外部图片 */
step('正在绘制金属与晶体纹理…');
function cv(size) {
  const c = document.createElement('canvas');
  c.width = c.height = size;
  return { c, x: c.getContext('2d') };
}
function tex(canvasEl, repeat = [1, 1], srgb = false) {
  const t = new CanvasTexture(canvasEl);
  t.wrapS = t.wrapT = repeat[0] < 0 ? MirroredRepeatWrapping : RepeatWrapping;
  t.repeat.set(Math.abs(repeat[0]), repeat[1]);
  t.anisotropy = Math.min(8, renderer.capabilities.getMaxAnisotropy());
  if (srgb) t.colorSpace = SRGBColorSpace;
  t.needsUpdate = true;
  return t;
}
/* 拉丝金属法线贴图 */
const TEX_METAL_N = (() => {
  const { c, x } = cv(512);
  x.fillStyle = '#8080ff'; x.fillRect(0, 0, 512, 512);
  for (let i = 0; i < 2600; i++) {
    const y = Math.random() * 512, w = 40 + Math.random() * 320, xx = Math.random() * 512;
    const v = 118 + Math.floor(Math.random() * 22);
    x.strokeStyle = `rgba(${v},${255 - v},${190 + Math.random() * 60},.55)`;
    x.lineWidth = 0.5 + Math.random() * 1.1;
    x.beginPath(); x.moveTo(xx, y); x.lineTo(xx + w, y + (Math.random() - 0.5) * 2.2); x.stroke();
  }
  return tex(c, [3, 3]);
})();
/* 金属粗糙度贴图(含锈斑与磨痕) */
const TEX_METAL_R = (() => {
  const { c, x } = cv(512);
  x.fillStyle = '#6a6a6a'; x.fillRect(0, 0, 512, 512);
  for (let i = 0; i < 340; i++) {
    const r = 4 + Math.random() * 44;
    const g = x.createRadialGradient(r * 2, r * 2, 0, r * 2, r * 2, r);
    const v = Math.random() < 0.5 ? 250 : 40;
    g.addColorStop(0, `rgba(${v},${v},${v},.32)`);
    g.addColorStop(1, 'rgba(0,0,0,0)');
    x.fillStyle = g;
    x.beginPath(); x.arc(Math.random() * 512, Math.random() * 512, r, 0, 7); x.fill();
  }
  for (let i = 0; i < 700; i++) {
    x.strokeStyle = `rgba(255,255,255,${Math.random() * 0.14})`;
    x.lineWidth = Math.random() * 1.4;
    const y = Math.random() * 512;
    x.beginPath(); x.moveTo(0, y); x.lineTo(512, y + (Math.random() - 0.5) * 8); x.stroke();
  }
  return tex(c, [2, 2]);
})();
/* 齿轮基座刻纹 */
const TEX_PLATE = (() => {
  const { c, x } = cv(1024);
  const S = 1024, R = S / 2;
  x.fillStyle = '#0b0e13'; x.fillRect(0, 0, S, S);
  x.save(); x.translate(R, R);
  const ring = (r, w, col, dash) => {
    x.strokeStyle = col; x.lineWidth = w;
    x.setLineDash(dash || []);
    x.beginPath(); x.arc(0, 0, r, 0, Math.PI * 2); x.stroke();
  };
  x.setLineDash([]);
  const g1 = x.createRadialGradient(0, 0, 10, 0, 0, R);
  g1.addColorStop(0, 'rgba(90,105,125,.55)');
  g1.addColorStop(0.55, 'rgba(35,44,56,.35)');
  g1.addColorStop(1, 'rgba(6,8,12,.9)');
  x.fillStyle = g1; x.beginPath(); x.arc(0, 0, R, 0, 7); x.fill();
  ring(R * 0.965, 5, 'rgba(150,175,200,.5)');
  ring(R * 0.925, 2, 'rgba(120,145,170,.32)');
  ring(R * 0.80, 3, 'rgba(140,165,190,.34)');
  ring(R * 0.55, 2, 'rgba(140,165,190,.28)');
  ring(R * 0.30, 4, 'rgba(160,185,210,.4)');
  ring(R * 0.13, 2, 'rgba(120,145,170,.3)');
  /* 外圈轮齿 */
  const teeth = 96;
  for (let i = 0; i < teeth; i++) {
    const a = (i / teeth) * Math.PI * 2;
    x.save(); x.rotate(a);
    x.fillStyle = i % 8 === 0 ? 'rgba(190,215,240,.62)' : 'rgba(130,155,180,.34)';
    x.fillRect(R * 0.965, -(i % 8 === 0 ? 7 : 3.5), R * 0.03, (i % 8 === 0 ? 14 : 7));
    x.restore();
  }
  /* 刻度线 */
  for (let i = 0; i < 360; i++) {
    const a = (i / 360) * Math.PI * 2, long = i % 15 === 0;
    x.save(); x.rotate(a);
    x.strokeStyle = long ? 'rgba(200,225,250,.55)' : 'rgba(150,175,200,.22)';
    x.lineWidth = long ? 2.4 : 1;
    x.beginPath();
    x.moveTo(R * 0.855, 0); x.lineTo(R * (long ? 0.905 : 0.885), 0);
    x.stroke(); x.restore();
  }
  /* 辐条与铆钉 */
  for (let i = 0; i < 12; i++) {
    const a = (i / 12) * Math.PI * 2;
    x.save(); x.rotate(a);
    x.strokeStyle = 'rgba(140,165,190,.3)'; x.lineWidth = 9;
    x.beginPath(); x.moveTo(R * 0.30, 0); x.lineTo(R * 0.795, 0); x.stroke();
    x.strokeStyle = 'rgba(200,225,250,.2)'; x.lineWidth = 1.5;
    x.beginPath(); x.moveTo(R * 0.30, -4.5); x.lineTo(R * 0.795, -4.5); x.stroke();
    for (let k = 0; k < 3; k++) {
      const rr = R * (0.40 + k * 0.17);
      const gr = x.createRadialGradient(rr, -2, 0, rr, 0, 8);
      gr.addColorStop(0, 'rgba(230,240,250,.85)');
      gr.addColorStop(1, 'rgba(60,75,95,.15)');
      x.fillStyle = gr; x.beginPath(); x.arc(rr, 0, 7, 0, 7); x.fill();
    }
    x.restore();
  }
  /* 内圈符文刻度 */
  x.font = '600 15px ui-monospace, Menlo, Consolas, monospace';
  x.textAlign = 'center'; x.textBaseline = 'middle';
  const runes = ['Ⅰ','Ⅱ','Ⅲ','Ⅳ','Ⅴ','Ⅵ','Ⅶ','Ⅷ','Ⅸ','Ⅹ','Ⅺ','Ⅻ'];
  for (let i = 0; i < 12; i++) {
    const a = (i / 12) * Math.PI * 2 - Math.PI / 2;
    const rr = R * 0.70;
    x.fillStyle = 'rgba(210,230,250,.5)';
    x.fillText(runes[i], Math.cos(a) * rr, Math.sin(a) * rr);
  }
  /* 中心发条 */
  for (let i = 0; i < 7; i++) {
    x.strokeStyle = `rgba(180,205,230,${0.34 - i * 0.035})`;
    x.lineWidth = 2.6;
    x.beginPath();
    x.arc(0, 0, R * (0.27 - i * 0.028), i * 1.15, i * 1.15 + 2.5);
    x.stroke();
  }
  /* 同心噪点 */
  for (let i = 0; i < 26000; i++) {
    const a = Math.random() * 7, r = Math.sqrt(Math.random()) * R;
    x.fillStyle = `rgba(255,255,255,${Math.random() * 0.05})`;
    x.fillRect(Math.cos(a) * r, Math.sin(a) * r, 1, 1);
  }
  x.restore();
  return tex(c, [1, 1], true);
})();
/* 翅膜纹样:脉络 + 蜂窝 + 边缘刻度 */
const TEX_MEMBRANE = (() => {
  const { c, x } = cv(512);
  x.clearRect(0, 0, 512, 512);
  const grad = x.createLinearGradient(0, 0, 512, 512);
  grad.addColorStop(0, 'rgba(255,255,255,.82)');
  grad.addColorStop(0.5, 'rgba(255,255,255,.42)');
  grad.addColorStop(1, 'rgba(255,255,255,.14)');
  x.fillStyle = grad; x.fillRect(0, 0, 512, 512);
  /* 蜂窝晶格 */
  const r = 26, hh = Math.sqrt(3) * r;
  x.strokeStyle = 'rgba(255,255,255,.16)'; x.lineWidth = 1.4;
  for (let row = -1; row < 512 / hh + 1; row++) {
    for (let col = -1; col < 512 / (r * 1.5) + 1; col++) {
      const cx = col * r * 1.5, cy = row * hh + (col % 2 ? hh / 2 : 0);
      x.beginPath();
      for (let i = 0; i < 6; i++) {
        const a = (i / 6) * Math.PI * 2;
        const px = cx + Math.cos(a) * r * 0.86, py = cy + Math.sin(a) * r * 0.86;
        i ? x.lineTo(px, py) : x.moveTo(px, py);
      }
      x.closePath(); x.stroke();
    }
  }
  /* 放射状翅脉 */
  x.strokeStyle = 'rgba(255,255,255,.34)';
  for (let i = 0; i < 9; i++) {
    x.lineWidth = 5 - i * 0.4;
    x.beginPath();
    x.moveTo(20, 256);
    x.bezierCurveTo(180, 256 + (i - 4) * 46, 360, 256 + (i - 4) * 82, 500, 256 + (i - 4) * 58);
    x.stroke();
  }
  /* 横脉 */
  x.strokeStyle = 'rgba(255,255,255,.14)'; x.lineWidth = 1.8;
  for (let i = 0; i < 14; i++) {
    const px = 40 + i * 34;
    x.beginPath(); x.moveTo(px, 10); x.lineTo(px + 24, 502); x.stroke();
  }
  /* 边缘渐隐 */
  const vg = x.createRadialGradient(256, 256, 90, 256, 256, 340);
  vg.addColorStop(0, 'rgba(0,0,0,0)');
  vg.addColorStop(1, 'rgba(0,0,0,.72)');
  x.fillStyle = vg; x.fillRect(0, 0, 512, 512);
  return tex(c, [1, 1]);
})();
/* 复眼晶格 */
const TEX_EYE = (() => {
  const { c, x } = cv(256);
  x.fillStyle = '#05070a'; x.fillRect(0, 0, 256, 256);
  const r = 9, hh = Math.sqrt(3) * r;
  for (let row = -1; row < 256 / hh + 1; row++) {
    for (let col = -1; col < 256 / (r * 1.5) + 1; col++) {
      const cx = col * r * 1.5, cy = row * hh + (col % 2 ? hh / 2 : 0);
      const g = x.createRadialGradient(cx - 2, cy - 2, 0, cx, cy, r);
      g.addColorStop(0, 'rgba(255,225,170,.95)');
      g.addColorStop(0.55, 'rgba(200,120,40,.5)');
      g.addColorStop(1, 'rgba(10,14,20,.9)');
      x.fillStyle = g;
      x.beginPath();
      for (let i = 0; i < 6; i++) {
        const a = (i / 6) * Math.PI * 2;
        const px = cx + Math.cos(a) * r * 0.94, py = cy + Math.sin(a) * r * 0.94;
        i ? x.lineTo(px, py) : x.moveTo(px, py);
      }
      x.closePath(); x.fill();
    }
  }
  return tex(c, [3, 2], true);
})();
/* 软光点精灵 */
const TEX_GLOW = (() => {
  const { c, x } = cv(128);
  const g = x.createRadialGradient(64, 64, 0, 64, 64, 64);
  g.addColorStop(0, 'rgba(255,255,255,1)');
  g.addColorStop(0.22, 'rgba(255,255,255,.62)');
  g.addColorStop(0.55, 'rgba(255,255,255,.16)');
  g.addColorStop(1, 'rgba(255,255,255,0)');
  x.fillStyle = g; x.fillRect(0, 0, 128, 128);
  return tex(c, [1, 1]);
})();
/* 地面光池 */
const TEX_POOL = (() => {
  const { c, x } = cv(512);
  const g = x.createRadialGradient(256, 256, 0, 256, 256, 256);
  g.addColorStop(0, 'rgba(255,255,255,.9)');
  g.addColorStop(0.35, 'rgba(255,255,255,.3)');
  g.addColorStop(0.72, 'rgba(255,255,255,.06)');
  g.addColorStop(1, 'rgba(255,255,255,0)');
  x.fillStyle = g; x.fillRect(0, 0, 512, 512);
  return tex(c, [1, 1]);
})();

/* SECTION: materials */
step('正在调配金属与琉璃材质…');
const MAT = {
  metal: new MeshStandardMaterial({
    color: T().metal, metalness: 1, roughness: 0.29,
    normalMap: TEX_METAL_N, normalScale: new Vector2(0.42, 0.42),
    roughnessMap: TEX_METAL_R, envMapIntensity: 1.15
  }),
  metalDark: new MeshStandardMaterial({
    color: T().metal3, metalness: 1, roughness: 0.44,
    normalMap: TEX_METAL_N, normalScale: new Vector2(0.55, 0.55),
    roughnessMap: TEX_METAL_R, envMapIntensity: 0.95
  }),
  metalBright: new MeshStandardMaterial({
    color: T().metal2, metalness: 1, roughness: 0.16, envMapIntensity: 1.5
  }),
  copper: new MeshStandardMaterial({
    color: T().copper, metalness: 1, roughness: 0.33,
    normalMap: TEX_METAL_N, normalScale: new Vector2(0.35, 0.35), envMapIntensity: 1.25
  }),
  shell: new MeshPhysicalMaterial({
    color: T().shell, metalness: 0.72, roughness: 0.36,
    clearcoat: 1, clearcoatRoughness: 0.16, envMapIntensity: 1.35
  }),
  dark: new MeshStandardMaterial({
    color: T().dark, metalness: 0.85, roughness: 0.58, envMapIntensity: 0.75
  }),
  glass: new MeshPhysicalMaterial({
    color: T().membrane, metalness: 0, roughness: 0.075,
    transmission: 0.93, thickness: 0.55, ior: 1.53,
    transparent: true, opacity: 0.62, side: DoubleSide,
    attenuationColor: new Color(T().membrane), attenuationDistance: 2.6,
    iridescence: 1, iridescenceIOR: 1.32,
    iridescenceThicknessRange: [110, 430],
    envMapIntensity: 1.75, depthWrite: false
  }),
  membrane: new MeshPhysicalMaterial({
    color: T().membrane, metalness: 0.1, roughness: 0.3,
    transparent: true, opacity: 0.4, alphaMap: TEX_MEMBRANE,
    side: DoubleSide, depthWrite: false,
    emissive: new Color(T().membrane), emissiveIntensity: T().membraneEmis,
    emissiveMap: TEX_MEMBRANE, envMapIntensity: 1.2,
    blending: AdditiveBlending
  }),
  vein: new MeshStandardMaterial({
    color: T().metal, metalness: 1, roughness: 0.24, envMapIntensity: 1.45
  }),
  eye: new MeshPhysicalMaterial({
    color: 0x1a1208, metalness: 0.25, roughness: 0.09,
    map: TEX_EYE, emissiveMap: TEX_EYE,
    emissive: new Color(T().accent), emissiveIntensity: 1.5,
    clearcoat: 1, clearcoatRoughness: 0.04, envMapIntensity: 1.9
  }),
  core: new MeshBasicMaterial({ color: T().core, toneMapped: false }),
  coreShell: new MeshPhysicalMaterial({
    color: 0x0a0d12, metalness: 0.4, roughness: 0.06,
    transmission: 0.85, thickness: 0.7, ior: 1.6, transparent: true,
    envMapIntensity: 2.2, clearcoat: 1
  }),
  plate: new MeshStandardMaterial({
    color: T().plate, metalness: 0.92, roughness: 0.52,
    map: TEX_PLATE, emissiveMap: TEX_PLATE,
    emissive: new Color(T().accent), emissiveIntensity: 0.22, envMapIntensity: 0.85
  }),
  plateSide: new MeshStandardMaterial({
    color: T().plate, metalness: 0.9, roughness: 0.6, envMapIntensity: 0.7
  }),
  xray: new MeshBasicMaterial({
    color: T().accent2, wireframe: true, transparent: true, opacity: 0.5,
    blending: AdditiveBlending, depthWrite: false, toneMapped: false
  }),
  xraySoft: new MeshBasicMaterial({
    color: T().accent2, transparent: true, opacity: 0.14,
    blending: AdditiveBlending, depthWrite: false, side: DoubleSide, toneMapped: false
  }),
  glowSprite: new SpriteMaterial({
    map: TEX_GLOW, color: T().accent, transparent: true, opacity: 0.85,
    blending: AdditiveBlending, depthWrite: false, toneMapped: false
  })
};
/* SECTION: theme-engine — 主题间所有颜色与强度按帧插值过渡 */
const THEME_COLOR_KEYS = ['accent', 'accent2', 'metal', 'metal2', 'metal3', 'dark', 'shell',
  'copper', 'core', 'membrane', 'bgTop', 'bgBot', 'bgGlow', 'fog', 'key', 'rim', 'fill',
  'hemiSky', 'hemiGnd', 'plate'];
const THEME_NUM_KEYS = ['keyI', 'rimI', 'fillI', 'corePower', 'membraneEmis', 'envI'];
const themeCur = {}, themeNum = {};
THEME_COLOR_KEYS.forEach((k) => { themeCur[k] = new Color(THEMES.brass[k]); });
THEME_NUM_KEYS.forEach((k) => { themeNum[k] = THEMES.brass[k]; });
let themeFromC = {}, themeFromN = {}, themeToKey = 'brass', themeMix = 1;
const _tc = new Color();
/* 场景中所有会发光的精灵材质(含基座、触角、翅尖) */
const spriteMats = [];
function collectSpriteMats() {
  spriteMats.length = 0;
  scene.traverse((o) => {
    if (o.isSprite && o.material && !spriteMats.includes(o.material)) spriteMats.push(o.material);
  });
}
function applyThemeToScene() {
  const c = themeCur, n = themeNum;
  MAT.metal.color.copy(c.metal);
  MAT.metalDark.color.copy(c.metal3);
  MAT.metalBright.color.copy(c.metal2);
  MAT.copper.color.copy(c.copper);
  MAT.shell.color.copy(c.shell);
  MAT.dark.color.copy(c.dark);
  MAT.vein.color.copy(c.metal);
  MAT.glass.color.copy(c.membrane);
  MAT.glass.attenuationColor.copy(c.membrane);
  MAT.membrane.color.copy(c.membrane);
  MAT.membrane.emissive.copy(c.membrane);
  MAT.membrane.emissiveIntensity = n.membraneEmis;
  MAT.eye.emissive.copy(c.accent);
  MAT.plate.color.copy(c.plate);
  MAT.plate.emissive.copy(c.accent);
  MAT.plateSide.color.copy(c.plate);
  MAT.xray.color.copy(c.accent2);
  MAT.glowSprite.color.copy(c.accent);
  for (const m of spriteMats) m.color.copy(c.accent);
  if (rig && rig.pool) rig.pool.material.color.copy(c.accent);
  MAT.metal.envMapIntensity = n.envI;
  MAT.metalBright.envMapIntensity = n.envI * 1.3;
  MAT.copper.envMapIntensity = n.envI * 1.08;
  MAT.shell.envMapIntensity = n.envI * 1.17;
  keyLight.color.copy(c.key); keyLight.intensity = n.keyI;
  rimLight.color.copy(c.rim); rimLight.intensity = n.rimI;
  fillLight.color.copy(c.fill); fillLight.intensity = n.fillI;
  hemi.color.copy(c.hemiSky); hemi.groundColor.copy(c.hemiGnd);
  coreLight.color.copy(c.core);
  wingLightL.color.copy(c.accent); wingLightR.color.copy(c.accent);
  bgUniforms.cTop.value.copy(c.bgTop);
  bgUniforms.cBot.value.copy(c.bgBot);
  bgUniforms.cGlow.value.copy(c.bgGlow);
  pUniforms.uColorA.value.copy(c.accent);
  pUniforms.uColorB.value.copy(c.accent2);
  scene.fog.color.copy(c.fog);
  renderer.setClearColor(c.bgBot);
}
function updateTheme(dt) {
  if (themeMix >= 1) return;
  themeMix = Math.min(1, themeMix + dt * 1.25);
  const k = themeMix * themeMix * (3 - 2 * themeMix);
  const B = THEMES[themeToKey];
  for (const key of THEME_COLOR_KEYS) themeCur[key].copy(themeFromC[key]).lerp(_tc.setHex(B[key]), k);
  for (const key of THEME_NUM_KEYS) themeNum[key] = themeFromN[key] + (B[key] - themeFromN[key]) * k;
  applyThemeToScene();
}
function applyTheme(k) {
  if (!THEMES[k]) return;
  themeFromC = {}; themeFromN = {};
  THEME_COLOR_KEYS.forEach((key) => { themeFromC[key] = themeCur[key].clone(); });
  THEME_NUM_KEYS.forEach((key) => { themeFromN[key] = themeNum[key]; });
  themeToKey = k;
  themeMix = 0;
  const t = THEMES[k];
  const rs = document.documentElement.style;
  rs.setProperty('--accent', '#' + new Color(t.accent).getHexString());
  rs.setProperty('--accent-2', '#' + new Color(t.accent2).getHexString());
  [...document.querySelectorAll('.theme')].forEach((b) => b.classList.toggle('is-on', b.dataset.theme === k));
  camState.shake = 0.04;
  updateTheme(1 / 60);
}

/* 材质注册表:用于结构透视时替换与还原 */
const solidMats = [MAT.metal, MAT.metalDark, MAT.metalBright, MAT.copper, MAT.shell, MAT.dark, MAT.eye, MAT.plate, MAT.plateSide, MAT.coreShell];
const savedMat = new Map();
function setXray(on) {
  scene.traverse((o) => {
    if (!o.isMesh || o.userData.noXray) return;
    const m = o.material;
    if (!solidMats.includes(m)) return;
    if (on) {
      if (!savedMat.has(o)) savedMat.set(o, m);
      o.material = MAT.xray;
      o.castShadow = false;
    } else {
      const back = savedMat.get(o);
      if (back) { o.material = back; o.castShadow = o.userData.shadow !== false; }
    }
  });
}

/* SECTION: utils */
function gearMesh(r, teeth, th, mat, opts = {}) {
  const s = new Shape();
  const n = Math.max(8, teeth | 0);
  const ri = r * (opts.rootRatio || 0.80);
  const ro = r;
  const stepA = (Math.PI * 2) / (n * 4);
  for (let i = 0; i < n * 4; i++) {
    const ph = i % 4;
    const rad = ph === 0 ? ri : ph === 1 ? ro : ph === 2 ? ro : ri;
    const a = i * stepA;
    const px = Math.cos(a) * rad, py = Math.sin(a) * rad;
    i ? s.lineTo(px, py) : s.moveTo(px, py);
  }
  s.closePath();
  const bore = opts.bore !== undefined ? opts.bore : r * 0.19;
  s.holes.push(new Path().absarc(0, 0, bore, 0, Math.PI * 2, true));
  if (opts.spokes) {
    const k = opts.spokes;
    for (let i = 0; i < k; i++) {
      const a = (i / k) * Math.PI * 2;
      const w = r * 0.085;
      const p = new Path();
      p.moveTo(Math.cos(a) * bore * 1.7 - Math.sin(a) * w, Math.sin(a) * bore * 1.7 + Math.cos(a) * w);
      p.lineTo(Math.cos(a) * ri * 0.86 - Math.sin(a) * w * 1.5, Math.sin(a) * ri * 0.86 + Math.cos(a) * w * 1.5);
      p.lineTo(Math.cos(a) * ri * 0.86 + Math.sin(a) * w * 1.5, Math.sin(a) * ri * 0.86 - Math.cos(a) * w * 1.5);
      p.lineTo(Math.cos(a) * bore * 1.7 + Math.sin(a) * w, Math.sin(a) * bore * 1.7 - Math.cos(a) * w);
      p.closePath();
      s.holes.push(p);
    }
  }
  const g = new ExtrudeGeometry(s, {
    depth: th, bevelEnabled: true, bevelThickness: th * 0.13,
    bevelSize: r * 0.026, bevelSegments: 2, curveSegments: 26, steps: 1
  });
  g.translate(0, 0, -th / 2);
  g.computeVertexNormals();
  const m = new Mesh(g, mat || MAT.metal);
  m.castShadow = true; m.receiveShadow = true;
  return m;
}
function rivetRing(parent, radius, count, size, mat, plane = 'xy', zOff = 0) {
  const g = new SphereGeometry(size, 9, 7);
  const inst = new InstancedMesh(g, mat || MAT.metalBright, count);
  inst.castShadow = true;
  const d = new Object3D();
  for (let i = 0; i < count; i++) {
    const a = (i / count) * Math.PI * 2;
    const x = Math.cos(a) * radius, y = Math.sin(a) * radius;
    if (plane === 'xy') d.position.set(x, y, zOff);
    else d.position.set(x, zOff, y);
    d.rotation.set(0, 0, 0);
    d.scale.set(1, 1, 0.62);
    if (plane === 'xz') d.rotateX(Math.PI / 2);
    d.updateMatrix();
    inst.setMatrixAt(i, d.matrix);
  }
  inst.instanceMatrix.needsUpdate = true;
  parent.add(inst);
  return inst;
}
function tubeAlong(curve, radius, mat, seg = 90, radial = 8) {
  const g = new TubeGeometry(curve, seg, radius, radial, false);
  const m = new Mesh(g, mat);
  m.castShadow = true;
  return m;
}
function capsuleAlong(a, b, radius, mat) {
  const dir = new Vector3().subVectors(b, a);
  const len = dir.length();
  const g = new CylinderGeometry(radius, radius, len, 12, 1, false);
  const m = new Mesh(g, mat);
  m.position.copy(a).addScaledVector(dir, 0.5);
  m.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir.clone().normalize());
  m.castShadow = true;
  return m;
}
function ball(r, mat, seg = 16) {
  const m = new Mesh(new SphereGeometry(r, seg, Math.max(10, seg - 4)), mat);
  m.castShadow = true;
  return m;
}
const clock = new Clock();

/* SECTION: tube-builder — 自定义变截面管,用于锥形翅脉与连杆 */
function buildTube(curve, segs, radial, rFn, closed = false) {
  const frames = curve.computeFrenetFrames(segs, closed);
  const pos = [], nor = [], uv = [], idx = [];
  for (let i = 0; i <= segs; i++) {
    const t = i / segs;
    const P = curve.getPointAt(t);
    const N = frames.normals[i], B = frames.binormals[i];
    const r = Math.max(1e-4, rFn(t));
    for (let j = 0; j <= radial; j++) {
      const a = (j / radial) * Math.PI * 2;
      const s = Math.sin(a), c = -Math.cos(a);
      const nx = c * N.x + s * B.x, ny = c * N.y + s * B.y, nz = c * N.z + s * B.z;
      nor.push(nx, ny, nz);
      pos.push(P.x + r * nx, P.y + r * ny, P.z + r * nz);
      uv.push(t * 4, j / radial);
    }
  }
  for (let i = 1; i <= segs; i++) {
    for (let j = 1; j <= radial; j++) {
      const a = (radial + 1) * (i - 1) + (j - 1);
      const b = (radial + 1) * i + (j - 1);
      const c = (radial + 1) * i + j;
      const d = (radial + 1) * (i - 1) + j;
      idx.push(a, b, d, b, c, d);
    }
  }
  const g = new BufferGeometry();
  g.setAttribute('position', new Float32BufferAttribute(pos, 3));
  g.setAttribute('normal', new Float32BufferAttribute(nor, 3));
  g.setAttribute('uv', new Float32BufferAttribute(uv, 2));
  g.setIndex(idx);
  return g;
}
function placeGeo(g, p, r, s) {
  const m = new Matrix4();
  const q = new Quaternion().setFromEuler(new Euler(r ? r[0] : 0, r ? r[1] : 0, r ? r[2] : 0));
  m.compose(new Vector3(p ? p[0] : 0, p ? p[1] : 0, p ? p[2] : 0), q,
    new Vector3(s ? s[0] : 1, s ? s[1] : 1, s ? s[2] : 1));
  return g.applyMatrix4(m);
}
const KEEP = ['position', 'normal', 'uv'];
function prep(g) {
  let o = g.index ? g.toNonIndexed() : g;
  for (const name of Object.keys(o.attributes)) if (!KEEP.includes(name)) o.deleteAttribute(name);
  if (!o.attributes.normal) o.computeVertexNormals();
  if (!o.attributes.uv) {
    const n = o.attributes.position.count;
    o.setAttribute('uv', new Float32BufferAttribute(new Float32Array(n * 2), 2));
  }
  return o;
}
function mergeTo(list, mat, cast = true, recv = true) {
  const src = list.filter(Boolean).map(prep);
  if (!src.length) return null;
  const merged = mergeGeometries(src, false);
  src.forEach((g) => g.dispose());
  if (!merged) return null;
  merged.computeBoundingSphere();
  const m = new Mesh(merged, mat);
  m.castShadow = cast; m.receiveShadow = recv;
  return m;
}
/* 把 ShapeGeometry 的 uv 归一化到 0..1,保证纹理比例一致 */
function fitUV(g, sx = 1, sy = 1) {
  g.computeBoundingBox();
  const bb = g.boundingBox;
  const w = Math.max(1e-4, bb.max.x - bb.min.x), h = Math.max(1e-4, bb.max.y - bb.min.y);
  const arr = g.attributes.uv.array;
  for (let i = 0; i < arr.length; i += 2) {
    arr[i] = ((arr[i] - bb.min.x) / w) * sx;
    arr[i + 1] = ((arr[i + 1] - bb.min.y) / h) * sy;
  }
  g.attributes.uv.needsUpdate = true;
  return g;
}
/* 翅膀局部平面:u = 展向(+X),v = 前向(-Z),翼面法线 +Y */
const P2 = (u, v, y = 0) => new Vector3(u, y, -v);

function nearestU(curve, target) {
  let best = 0, bd = Infinity;
  for (let i = 0; i <= 420; i++) {
    const u = i / 420, d = curve.getPointAt(u).distanceToSquared(target);
    if (d < bd) { bd = d; best = u; }
  }
  for (let k = 0; k < 3; k++) {
    const span = 1 / 90 / (k + 2);
    let b2 = best, d2 = Infinity;
    for (let i = -10; i <= 10; i++) {
      const u = MathUtils.clamp(best + i * span, 0, 1);
      const d = curve.getPointAt(u).distanceToSquared(target);
      if (d < d2) { d2 = d; b2 = u; }
    }
    best = b2;
  }
  return best;
}

/* SECTION: wing-data */
const FORE = {
  outline: [[0.05, 0.44], [0.62, 0.98], [1.35, 1.40], [2.20, 1.63], [3.10, 1.50], [3.95, 1.08],
    [4.48, 0.50], [4.62, -0.14], [4.20, -0.64], [3.45, -0.93], [2.55, -1.01], [1.60, -0.87],
    [0.85, -0.56], [0.02, -0.27]],
  veinEnds: [[0.78, 1.10], [1.92, 1.61], [3.06, 1.52], [4.02, 1.03], [4.56, 0.04],
    [3.92, -0.79], [2.72, -0.99], [1.46, -0.87], [0.44, -0.42]],
  bow: [0.10, 0.16, 0.13, 0.06, -0.02, -0.10, -0.16, -0.14, -0.06],
  root: [0.03, 0.06],
  sparTo: 0.44,
  pivot: [0, 0.07, -0.12],
  gears: [[0.42, 0.02, 0.30, 13], [1.05, 0.62, 0.20, 9], [2.35, 0.55, 0.17, 8], [1.70, -0.52, 0.22, 10]]
};
const HIND = {
  outline: [[0.02, 0.36], [0.52, 0.74], [1.22, 0.99], [2.02, 1.02], [2.86, 0.84], [3.46, 0.44],
    [3.80, -0.10], [3.84, -0.68], [3.58, -1.22], [3.02, -1.66], [2.28, -1.88], [1.48, -1.84],
    [0.84, -1.52], [0.34, -1.04], [0.02, -0.42]],
  veinEnds: [[0.95, 0.90], [2.02, 1.01], [2.98, 0.76], [3.72, 0.02], [3.62, -1.10],
    [2.90, -1.72], [1.86, -1.86], [0.92, -1.56]],
  bow: [0.14, 0.10, 0.04, -0.04, -0.12, -0.16, -0.12, -0.06],
  root: [0.02, -0.02],
  sparTo: 0.34,
  pivot: [0, -0.02, 0.44],
  gears: [[0.40, -0.06, 0.26, 12], [1.30, 0.34, 0.18, 8], [1.15, -1.05, 0.19, 9]]
};

/* SECTION: wing-builder */
step('正在编织翅脉桁架与琉璃翅室…');
function buildHalfWing(D, tag) {
  const out = {
    pivot: new Group(), parts: { gears: [], vanes: [], lights: [] },
    anchor: new Vector3(), cells: 0, veins: 0
  };
  const gVein = [], gMetal = [], gDark = [], gGlass = [], gMem = [], gCore = [];

  const outlinePts = D.outline.map(([u, v]) => P2(u, v));
  const outlineCurve = new CatmullRomCurve3(outlinePts, true, 'catmullrom', 0.42);

  /* 主轮廓梁 */
  gMetal.push(buildTube(outlineCurve, 220, 9,
    (t) => 0.052 + 0.020 * Math.sin(t * Math.PI * 2 * 3) + 0.012 * Math.cos(t * Math.PI * 8), true));
  gDark.push(buildTube(outlineCurve, 170, 6, () => 0.030, true));

  /* 前缘主梁(较粗) */
  const sparPts = [];
  for (let i = 0; i <= 34; i++) sparPts.push(outlineCurve.getPointAt((i / 34) * D.sparTo));
  const sparCurve = new CatmullRomCurve3(sparPts, false, 'catmullrom', 0.4);
  gMetal.push(buildTube(sparCurve, 90, 10, (t) => 0.085 * (1 - 0.55 * Math.pow(t, 0.8)) + 0.012));

  /* 翅脉:根部 → 轮廓 */
  const rootP = P2(D.root[0], D.root[1], 0.0);
  const veinCurves = [];
  const veinU = [];
  D.veinEnds.forEach((e, i) => {
    const end = outlineCurve.getPointAt(nearestU(outlineCurve, P2(e[0], e[1])));
    const dir = end.clone().sub(rootP);
    const perp = new Vector3(dir.z, 0, -dir.x).normalize();
    const bow = (D.bow[i] || 0);
    const c1 = rootP.clone().addScaledVector(dir, 0.36).addScaledVector(perp, bow * 0.55);
    const c2 = rootP.clone().addScaledVector(dir, 0.74).addScaledVector(perp, bow);
    const cv = new CatmullRomCurve3([rootP.clone(), c1, c2, end.clone()], false, 'catmullrom', 0.4);
    veinCurves.push(cv);
    veinU.push(nearestU(outlineCurve, end));
    gVein.push(buildTube(cv, 64, 8, (t) => 0.052 * (1 - 0.78 * Math.pow(t, 0.72)) + 0.008));
    /* 翅脉内发光导管 */
    gCore.push(buildTube(cv, 46, 5, (t) => 0.0165 * (1 - 0.6 * t) + 0.004));
  });

  /* 桁架横撑 */
  for (let i = 0; i < veinCurves.length - 1; i++) {
    for (let k = 1; k <= 3; k++) {
      const t = 0.18 + k * 0.235;
      const a = veinCurves[i].getPointAt(t), b = veinCurves[i + 1].getPointAt(t);
      if (a.distanceTo(b) < 0.09) continue;
      const mid = a.clone().lerp(b, 0.5);
      mid.y += (k % 2 ? 0.055 : -0.055);
      const cv = new CatmullRomCurve3([a, mid, b], false, 'catmullrom', 0.5);
      gVein.push(buildTube(cv, 22, 6, (tt) => 0.021 * (1 - 0.42 * Math.abs(tt - 0.5) * 2) + 0.005));
    }
  }

  /* 翅室(琉璃 + 膜 + 边框) */
  for (let i = 0; i < veinCurves.length - 1; i++) {
    const u0 = veinU[i], u1 = veinU[i + 1];
    let du = u1 - u0;
    if (du < 0) du += 1;
    const loop = [];
    for (let k = 0; k <= 9; k++) loop.push(veinCurves[i].getPointAt(k / 9));
    const m = Math.max(4, Math.ceil(du * 90));
    for (let k = 1; k < m; k++) loop.push(outlineCurve.getPointAt((u0 + du * (k / m)) % 1));
    for (let k = 9; k >= 0; k--) loop.push(veinCurves[i + 1].getPointAt(k / 9));

    const pts2 = loop.map((p) => new THREE.Vector2(p.x, -p.z));
    const shape = new Shape(pts2);
    const glass = new ShapeGeometry(shape, 1);
    fitUV(glass, 1.5, 1.5);
    glass.rotateX(-Math.PI / 2);
    glass.translate(0, 0.002, 0);
    gGlass.push(glass);

    const mem = new ShapeGeometry(shape, 1);
    fitUV(mem, 1.15, 1.15);
    mem.rotateX(-Math.PI / 2);
    mem.translate(0, 0.008, 0);
    gMem.push(mem);

    const cellCurve = new CatmullRomCurve3(loop, true, 'catmullrom', 0.24);
    gVein.push(buildTube(cellCurve, 96, 6, (t) => 0.024 + 0.008 * Math.sin(t * Math.PI * 6), true));
    out.cells++;
  }
  out.veins = veinCurves.length;

  /* 翅室中央加强肋 */
  for (let i = 0; i < veinCurves.length - 1; i++) {
    const t = 0.55;
    const a = veinCurves[i].getPointAt(t), b = veinCurves[i + 1].getPointAt(t);
    const mid = a.clone().lerp(b, 0.5);
    gDark.push(placeGeo(new BoxGeometry(0.055, 0.03, a.distanceTo(b) * 0.86),
      [mid.x, mid.y - 0.035, mid.z], [0, Math.atan2(-(b.z - a.z), b.x - a.x), 0]));
  }

  /* 铆钉环 */
  const rivetGeo = new SphereGeometry(0.026, 7, 5);
  const rivets = [];
  for (let i = 0; i < 76; i++) {
    const p = outlineCurve.getPointAt(i / 76);
    rivets.push(placeGeo(rivetGeo.clone(), [p.x, p.y + 0.045, p.z]));
  }
  const rivetMesh = mergeTo(rivets, MAT.metalBright, true, false);
  rivetGeo.dispose();

  /* 驱动齿轮 */
  const gears = [];
  D.gears.forEach(([gu, gv, gr, gt], i) => {
    const gm = gearMesh(gr, gt, 0.055 + gr * 0.12, i % 2 ? MAT.copper : MAT.metal,
      { bore: gr * 0.2, spokes: gt > 10 ? 5 : 0 });
    gm.rotation.x = Math.PI / 2;
    const p = P2(gu, gv, -0.062);
    gm.position.copy(p);
    gm.castShadow = true;
    gm.userData.anim = { type: 'spin', axis: 'z', speed: (i % 2 ? -1 : 1) * (1.5 + i * 0.32) };
    out.parts.gears.push(gm);
    gears.push(gm);
    /* 齿轮座与轴 */
    const hub = new Mesh(new CylinderGeometry(gr * 0.2, gr * 0.26, 0.1, 12), MAT.dark);
    hub.position.set(p.x, p.y - 0.05, p.z);
    hub.castShadow = true;
    gDark.push(placeGeo(new CylinderGeometry(gr * 0.2, gr * 0.26, 0.11, 12), [p.x, p.y - 0.052, p.z]));
    gMetal.push(placeGeo(new TorusGeometry(gr * 1.02, 0.016, 6, 30), [p.x, p.y + 0.05, p.z], [Math.PI / 2, 0, 0]));
    hub.geometry.dispose();
  });

  /* 根部铰链总成 */
  gMetal.push(placeGeo(new CylinderGeometry(0.135, 0.135, 0.30, 18), [0.06, 0.02, 0.0], [0, 0, Math.PI / 2]));
  gDark.push(placeGeo(new TorusGeometry(0.155, 0.028, 8, 22), [0.20, 0.02, 0.0], [0, Math.PI / 2, 0]));
  gDark.push(placeGeo(new TorusGeometry(0.155, 0.028, 8, 22), [-0.08, 0.02, 0.0], [0, Math.PI / 2, 0]));
  gMetal.push(placeGeo(new BoxGeometry(0.24, 0.16, 0.30), [0.06, 0.02, 0.0]));
  /* 作动筒 */
  const actA = new Vector3(0.02, -0.10, 0.26), actB = new Vector3(1.15, -0.05, 0.34);
  gMetal.push(buildTube(new CatmullRomCurve3([actA, actB], false), 8, 12, () => 0.055));
  gDark.push(buildTube(new CatmullRomCurve3([actA.clone().lerp(actB, 0.55), actB], false), 8, 10, () => 0.034));

  /* 翼尖传感器与爪 */
  const tipU = D.veinEnds[Math.floor(D.veinEnds.length / 2)];
  const tipP = P2(tipU[0], tipU[1]);
  const tip = outlineCurve.getPointAt(nearestU(outlineCurve, tipP));
  gMetal.push(placeGeo(new ConeGeometry(0.085, 0.34, 10), [tip.x + 0.16, tip.y, tip.z], [0, 0, -Math.PI / 2]));
  gDark.push(placeGeo(new TorusGeometry(0.075, 0.02, 6, 16), [tip.x + 0.34, tip.y, tip.z], [0, Math.PI / 2, 0]));
  const tipLight = new Sprite(MAT.glowSprite.clone());
  tipLight.scale.setScalar(0.42);
  tipLight.position.set(tip.x + 0.36, tip.y, tip.z);
  out.parts.lights.push(tipLight);

  /* 合并输出 */
  const add = (geo, mat, cast = true, recv = true) => {
    const m = mergeTo(geo, mat, cast, recv);
    if (m) { out.pivot.add(m); return m; }
    return null;
  };
  const veinMesh = add(gVein, MAT.vein);
  add(gMetal, MAT.metal);
  add(gDark, MAT.dark);
  const glassMesh = add(gGlass, MAT.glass, false, false);
  if (glassMesh) { glassMesh.renderOrder = 3; glassMesh.userData.noXray = true; }
  const memMesh = add(gMem, MAT.membrane, false, false);
  if (memMesh) { memMesh.renderOrder = 4; memMesh.userData.noXray = true; }
  const coreMesh = add(gCore, MAT.core, false, false);
  if (coreMesh) { coreMesh.renderOrder = 5; coreMesh.userData.noXray = true; }
  if (rivetMesh) out.pivot.add(rivetMesh);
  gears.forEach((g) => out.pivot.add(g));
  out.pivot.add(tipLight);

  out.anchor.copy(veinCurves[Math.floor(veinCurves.length / 2)].getPointAt(0.52));
  out.tag = tag;
  return out;
}

/* 后翅尾突与可调扇叶 */
function buildTails(parent, parts) {
  const tails = [
    { root: [2.28, -1.88], dir: [2.55, -3.05], ctrl: [2.62, -2.55], r: 0.036, pad: 0.12 },
    { root: [1.48, -1.84], dir: [1.16, -3.28], ctrl: [1.02, -2.60], r: 0.032, pad: 0.10 }
  ];
  tails.forEach((t, i) => {
    const a = P2(t.root[0], t.root[1]), c = P2(t.ctrl[0], t.ctrl[1]), b = P2(t.dir[0], t.dir[1]);
    const cv = new CatmullRomCurve3([a, c, b], false, 'catmullrom', 0.4);
    const tube = new Mesh(buildTube(cv, 46, 8, (u) => t.r * (1 - 0.62 * u) + 0.005), MAT.vein);
    tube.castShadow = true;
    parent.add(tube);
    /* 尾端桨叶 */
    const pivot = new Group();
    pivot.position.copy(b);
    const padShape = new Shape();
    padShape.moveTo(0, 0);
    padShape.bezierCurveTo(0.10, 0.10, 0.20, 0.16, 0.30, 0.02);
    padShape.bezierCurveTo(0.34, -0.10, 0.20, -0.20, 0.06, -0.14);
    padShape.closePath();
    const padGeo = new ShapeGeometry(padShape, 8);
    fitUV(padGeo, 1, 1);
    padGeo.rotateX(-Math.PI / 2);
    const pad = new Mesh(padGeo, MAT.membrane);
    pad.renderOrder = 4; pad.userData.noXray = true;
    pivot.add(pad);
    const rim = new Mesh(new TorusGeometry(t.pad, 0.018, 6, 18), MAT.metal);
    rim.position.set(0.16, 0, -0.03);
    rim.rotation.x = Math.PI / 2;
    rim.castShadow = true;
    pivot.add(rim);
    pivot.userData.anim = { type: 'vane', amp: 0.30, speed: 1.5 + i * 0.35, phase: i * 1.1 };
    parts.vanes.push(pivot);
    parent.add(pivot);
    const sp = new Sprite(MAT.glowSprite.clone());
    sp.scale.setScalar(0.3);
    sp.position.set(0.16, 0, -0.03);
    pivot.add(sp);
    parts.lights.push(sp);
  });
}

/* 后翅后缘可调扇叶 */
function buildVanes(parent, parts) {
  const anchors = [[3.58, -1.22], [3.02, -1.66], [2.28, -1.88], [1.48, -1.84], [0.84, -1.52]];
  anchors.forEach((a, i) => {
    const pivot = new Group();
    pivot.position.copy(P2(a[0], a[1], -0.01));
    pivot.rotation.y = -0.5 + i * 0.22;
    const vaneShape = new Shape();
    vaneShape.moveTo(0, 0);
    vaneShape.bezierCurveTo(0.16, 0.14, 0.42, 0.17, 0.56, 0.0);
    vaneShape.bezierCurveTo(0.42, -0.17, 0.16, -0.14, 0, 0);
    const vg = new ShapeGeometry(vaneShape, 8);
    fitUV(vg, 1.4, 1.4);
    vg.rotateX(-Math.PI / 2);
    const vane = new Mesh(vg, MAT.membrane);
    vane.renderOrder = 4; vane.userData.noXray = true;
    pivot.add(vane);
    const spine = new Mesh(buildTube(new CatmullRomCurve3(
      [new Vector3(0, 0, 0), new Vector3(0.30, 0.02, -0.10), new Vector3(0.56, 0, 0)], false, 'catmullrom', 0.4),
      22, 6, (t) => 0.019 * (1 - 0.6 * t) + 0.004), MAT.vein);
    spine.castShadow = true;
    pivot.add(spine);
    pivot.userData.anim = { type: 'vane', amp: 0.24, speed: 2.1 + i * 0.28, phase: i * 0.62 };
    parts.vanes.push(pivot);
    parent.add(pivot);
  });
}

/* SECTION: wing-assembly */
function buildWingAssembly() {
  const root = new Group();
  root.name = 'wingAssembly';
  const fore = buildHalfWing(FORE, 'fore');
  const hind = buildHalfWing(HIND, 'hind');
  buildTails(hind.pivot, hind.parts);
  buildVanes(hind.pivot, hind.parts);

  const forePivot = new Group();
  forePivot.name = 'forePivot';
  forePivot.position.set(fore.pivot.position.x, FORE.pivot[1], FORE.pivot[2]);
  forePivot.add(fore.pivot);
  const hindPivot = new Group();
  hindPivot.name = 'hindPivot';
  hindPivot.position.set(0, HIND.pivot[1], HIND.pivot[2]);
  hindPivot.add(hind.pivot);
  hindPivot.rotation.y = -0.13;

  const flap = new Group();
  flap.name = 'flap';
  flap.add(forePivot, hindPivot);
  root.add(flap);

  return {
    root, flap, forePivot, hindPivot,
    parts: [...fore.parts.gears, ...hind.parts.gears],
    vanes: [...fore.parts.vanes, ...hind.parts.vanes],
    lights: [...fore.parts.lights, ...hind.parts.lights],
    anchorFore: fore.anchor.clone(), anchorHind: hind.anchor.clone()
  };
}

/* SECTION: body-builder */
step('正在装配胸甲、腹节与六足关节…');
function IcosahedronSafe(r, d) {
  const C = THREE.IcosahedronGeometry || THREE.IcosahedronBufferGeometry;
  return new C(r, d);
}
function buildThorax() {
  const g = new Group();
  g.name = 'thorax';
  const gMetal = [], gDark = [], gShell = [], gCore = [];
  const gears = [], pistons = [], lights = [];

  /* 主胸甲:旋转体 */
  const profile = [];
  for (let i = 0; i <= 24; i++) {
    const t = i / 24;
    const z = -1.12 + t * 1.5;
    const r = 0.42 * Math.sin(Math.PI * Math.pow(t, 0.82)) * (1 - 0.16 * t) + 0.055;
    profile.push(new THREE.Vector2(Math.max(0.02, r), z));
  }
  const shellGeo = new LatheGeometry(profile, 38);
  shellGeo.rotateX(Math.PI / 2);
  gShell.push(shellGeo);

  /* 背甲分节环 + 脊线 */
  for (let i = 0; i < 6; i++) {
    const t = 0.12 + i * 0.145;
    const z = -1.12 + t * 1.5;
    const r = 0.42 * Math.sin(Math.PI * Math.pow(t, 0.82)) * (1 - 0.16 * t) + 0.06;
    gMetal.push(placeGeo(new TorusGeometry(r * 1.01, 0.032, 8, 32), [0, 0.02, z], [Math.PI / 2, 0, 0]));
  }
  gMetal.push(buildTube(new CatmullRomCurve3([
    new Vector3(0, 0.38, -0.98), new Vector3(0, 0.45, -0.4),
    new Vector3(0, 0.42, 0.08), new Vector3(0, 0.28, 0.38)
  ], false, 'catmullrom', 0.4), 46, 8, (t) => 0.034 * (1 - 0.4 * Math.abs(t - 0.5) * 2) + 0.012));

  /* 侧向散热栅 + 气门 */
  for (const side of [-1, 1]) {
    for (let i = 0; i < 7; i++) {
      const z = -0.76 + i * 0.155;
      const r = 0.40 * Math.sin(Math.PI * Math.pow((z + 1.12) / 1.5, 0.82)) + 0.05;
      gDark.push(placeGeo(new BoxGeometry(0.05, 0.05, 0.12), [side * r * 0.94, -0.02, z], [0, 0, side * 0.26]));
    }
    for (let i = 0; i < 3; i++) {
      gMetal.push(placeGeo(new CylinderGeometry(0.052, 0.052, 0.06, 14),
        [side * 0.385, 0.10, -0.52 + i * 0.40], [0, 0, Math.PI / 2]));
      gCore.push(placeGeo(new CylinderGeometry(0.030, 0.030, 0.07, 12),
        [side * 0.39, 0.10, -0.52 + i * 0.40], [0, 0, Math.PI / 2]));
    }
    /* 翅根驱动齿轮箱 */
    const housing = gearMesh(0.285, 20, 0.15, MAT.metalDark, { bore: 0.072, spokes: 5 });
    housing.rotation.set(Math.PI / 2, 0, 0);
    housing.position.set(side * 0.385, 0.13, -0.14);
    housing.userData.anim = { type: 'spinLocal', speed: side * 3.0 };
    gears.push(housing);
    g.add(housing);
    gMetal.push(placeGeo(new TorusGeometry(0.315, 0.024, 8, 32), [side * 0.385, 0.13, -0.14], [Math.PI / 2, 0, 0]));
    gDark.push(placeGeo(new CylinderGeometry(0.095, 0.125, 0.11, 16), [side * 0.385, 0.13, -0.28], [Math.PI / 2, 0, 0]));
    gMetal.push(buildTube(new CatmullRomCurve3([
      new Vector3(side * 0.385, 0.13, -0.14),
      new Vector3(side * 0.50, 0.02, 0.02),
      new Vector3(side * 0.42, -0.13, 0.17)
    ], false, 'catmullrom', 0.4), 28, 7, (t) => 0.026 + 0.005 * Math.sin(t * 20)));
  }

  /* 活塞作动筒 */
  for (const side of [-1, 1]) {
    const pg = new Group();
    const cyl = new Mesh(new CylinderGeometry(0.082, 0.082, 0.44, 16), MAT.metal);
    cyl.position.y = -0.22; cyl.castShadow = true;
    const rod = new Mesh(new CylinderGeometry(0.032, 0.032, 0.38, 12), MAT.metalBright);
    rod.position.y = -0.56; rod.castShadow = true;
    const cap = new Mesh(new SphereGeometry(0.052, 12, 9), MAT.dark);
    cap.position.y = -0.74;
    pg.add(cyl, rod, cap);
    pg.position.set(side * 0.28, 0.15, 0.0);
    pg.rotation.z = side * 0.32;
    pg.userData.anim = { type: 'piston', amp: 0.12 };
    pistons.push(pg);
    g.add(pg);
  }

  /* 胸部核心反应腔 */
  const coreShell = new Mesh(new SphereGeometry(0.225, 26, 20), MAT.coreShell);
  coreShell.position.set(0, 0.05, -0.08);
  coreShell.userData.noXray = true;
  g.add(coreShell);
  const coreBall = new Mesh(IcosahedronSafe(0.128, 2), MAT.core);
  coreBall.position.copy(coreShell.position);
  coreBall.userData.noXray = true;
  coreBall.userData.anim = { type: 'core' };
  g.add(coreBall);
  const coreGlow = new Sprite(MAT.glowSprite.clone());
  coreGlow.scale.setScalar(1.5);
  coreGlow.position.copy(coreShell.position);
  g.add(coreGlow);
  lights.push(coreGlow);
  coreLight.position.copy(coreShell.position);
  g.add(coreLight);
  gCore.push(placeGeo(new TorusGeometry(0.235, 0.020, 8, 28), [0, 0.05, -0.08], [Math.PI / 2.4, 0, 0]));
  gCore.push(placeGeo(new TorusGeometry(0.242, 0.016, 8, 28), [0, 0.05, -0.08], [Math.PI / 1.7, 0.6, 0]));
  for (let i = 0; i < 9; i++) {
    const a = (i / 9) * Math.PI;
    gMetal.push(placeGeo(new TorusGeometry(0.245, 0.013, 5, 26), [0, 0.05, -0.08], [0, a, 0]));
  }

  const add = (geo, mat, cast = true, recv = true) => {
    const m = mergeTo(geo, mat, cast, recv);
    if (m) g.add(m);
    return m;
  };
  add(gShell, MAT.shell);
  add(gMetal, MAT.metal);
  add(gDark, MAT.dark);
  const cm = add(gCore, MAT.core, false, false);
  if (cm) { cm.renderOrder = 2; cm.userData.noXray = true; }

  g.userData.gears = gears;
  g.userData.pistons = pistons;
  g.userData.lights = lights;
  g.userData.coreBall = coreBall;
  g.userData.coreShell = coreShell;
  return g;
}

function buildAbdomen() {
  const root = new Group();
  root.name = 'abdomen';
  const segs = [];
  const SEG_N = 9;
  for (let i = 0; i < SEG_N; i++) {
    const t = i / (SEG_N - 1);
    const seg = new Group();
    /* 腹节链式嵌套:首节相对腹部根,其余相对上一节,故用增量间距 */
    const z = i === 0 ? 0.40 : 0.315;
    const r = 0.33 * Math.sin(Math.PI * (0.16 + t * 0.80)) + 0.052;
    const gMetal = [], gDark = [], gCore = [], gShell = [];

    gMetal.push(placeGeo(new TorusGeometry(r * 1.02, 0.042, 9, 30), [0, 0, 0], [Math.PI / 2, 0, 0]));
    const platePts = [];
    for (let k = 0; k <= 14; k++) {
      const u = k / 14;
      platePts.push(new THREE.Vector2(Math.max(0.03, r * Math.sin(Math.PI * u * 0.94)), u * 0.28 - 0.02));
    }
    gShell.push(placeGeo(new LatheGeometry(platePts, 24), [0, 0, 0.02], [Math.PI / 2, 0, 0]));
    gDark.push(placeGeo(new BoxGeometry(r * 1.45, 0.026, 0.11), [0, r * 0.90, 0.05]));
    gMetal.push(placeGeo(new BoxGeometry(r * 0.86, 0.018, 0.045), [0, r * 0.95, 0.05]));
    for (const side of [-1, 1]) {
      gCore.push(placeGeo(new BoxGeometry(0.046, 0.042, 0.19), [side * r * 0.88, -0.02, 0.03]));
    }
    gDark.push(placeGeo(new CylinderGeometry(r * 0.80, r * 0.86, 0.105, 18, 1, true),
      [0, 0, -0.125], [Math.PI / 2, 0, 0]));
    for (let k = 0; k < 8; k++) {
      const a = (k / 8) * Math.PI * 2;
      gMetal.push(placeGeo(new SphereGeometry(0.021, 6, 5),
        [Math.cos(a) * r * 1.05, Math.sin(a) * r * 1.05, 0.012]));
    }

    const add = (geo, mat, cast = true, recv = true) => {
      const m = mergeTo(geo, mat, cast, recv);
      if (m) seg.add(m);
      return m;
    };
    add(gShell, MAT.shell);
    add(gMetal, MAT.metal);
    add(gDark, MAT.dark);
    const cm = add(gCore, MAT.core, false, false);
    if (cm) { cm.renderOrder = 2; cm.userData.noXray = true; }

    seg.position.set(0, 0, z);
    seg.userData.segIndex = i;
    segs.push(seg);
    if (i) segs[i - 1].add(seg); else root.add(seg);
  }

  /* 尾端 */
  const tail = segs[segs.length - 1];
  const gTail = [];
  for (let i = 0; i < 3; i++) {
    const a = (i / 3) * Math.PI * 2 + 0.4;
    gTail.push(buildTube(new CatmullRomCurve3([
      new Vector3(Math.cos(a) * 0.085, Math.sin(a) * 0.085, 0.04),
      new Vector3(Math.cos(a) * 0.14, Math.sin(a) * 0.14, 0.22),
      new Vector3(Math.cos(a) * 0.055, Math.sin(a) * 0.055, 0.42)
    ], false, 'catmullrom', 0.4), 28, 7, (t) => 0.028 * (1 - 0.7 * t) + 0.005));
  }
  gTail.push(placeGeo(new SphereGeometry(0.070, 14, 11), [0, 0, 0.09]));
  gTail.push(placeGeo(new ConeGeometry(0.046, 0.19, 10), [0, 0, 0.54], [Math.PI / 2, 0, 0]));
  const tailMesh = mergeTo(gTail, MAT.metal);
  if (tailMesh) tail.add(tailMesh);
  const tailGlow = new Sprite(MAT.glowSprite.clone());
  tailGlow.scale.setScalar(0.5);
  tailGlow.position.set(0, 0, 0.58);
  tail.add(tailGlow);

  root.userData.segs = segs;
  root.userData.tailGlow = tailGlow;
  return root;
}

function buildHead() {
  const g = new Group();
  g.name = 'head';
  const gMetal = [], gDark = [], gCore = [], lights = [];

  const headPts = [];
  for (let i = 0; i <= 20; i++) {
    const u = i / 20;
    headPts.push(new THREE.Vector2(
      Math.max(0.02, 0.30 * Math.sin(Math.PI * Math.pow(u, 0.72))), u * 0.44 - 0.28));
  }
  const headShell = new LatheGeometry(headPts, 32);
  headShell.rotateX(-Math.PI / 2);
  gMetal.push(headShell);

  /* 复眼 + 透镜组 */
  const eyes = [];
  for (const side of [-1, 1]) {
    const eye = new Mesh(new SphereGeometry(0.178, 28, 22), MAT.eye);
    eye.scale.set(0.72, 1.0, 1.08);
    eye.position.set(side * 0.225, 0.02, -0.02);
    eye.rotation.z = side * 0.16;
    eye.castShadow = true;
    g.add(eye);
    eyes.push(eye);
    gDark.push(placeGeo(new TorusGeometry(0.178, 0.025, 8, 28),
      [side * 0.225, 0.02, -0.02], [0, side * 0.16, Math.PI / 2], [0.72, 1.0, 1.08]));
    for (let i = 0; i < 3; i++) {
      gMetal.push(placeGeo(new CylinderGeometry(0.050 - i * 0.008, 0.050 - i * 0.008, 0.048, 12),
        [side * (0.29 + i * 0.048), 0.02 - i * 0.01, -0.02], [0, 0, Math.PI / 2]));
      gCore.push(placeGeo(new SphereGeometry(0.028 - i * 0.005, 10, 8),
        [side * (0.322 + i * 0.048), 0.02 - i * 0.01, -0.02]));
    }
    const sp = new Sprite(MAT.glowSprite.clone());
    sp.scale.setScalar(0.46);
    sp.position.set(side * 0.29, 0.02, -0.02);
    g.add(sp);
    lights.push(sp);
  }

  /* 额部传感器 + 口器基座 */
  gDark.push(placeGeo(new SphereGeometry(0.080, 16, 12), [0, 0.02, -0.265]));
  gCore.push(placeGeo(new SphereGeometry(0.042, 12, 10), [0, 0.02, -0.312]));
  gMetal.push(placeGeo(new CylinderGeometry(0.095, 0.12, 0.095, 18), [0, -0.125, -0.155], [0.35, 0, 0]));
  gMetal.push(placeGeo(new TorusGeometry(0.103, 0.017, 6, 20), [0, -0.168, -0.170], [Math.PI / 2 + 0.35, 0, 0]));

  /* 螺旋口器 */
  const spiPts = [];
  for (let i = 0; i <= 150; i++) {
    const t = i / 150;
    const ang = t * Math.PI * 5.4;
    const r = 0.285 * Math.pow(1 - t, 1.25) + 0.026;
    spiPts.push(new Vector3(
      Math.cos(ang) * r,
      -0.19 - t * 0.055 + Math.sin(ang) * r * 0.42,
      -0.19 - t * 0.40 + Math.sin(ang) * r));
  }
  const spiCurve = new CatmullRomCurve3(spiPts, false, 'catmullrom', 0.3);
  gMetal.push(buildTube(spiCurve, 165, 8, (t) => 0.030 * (1 - 0.72 * t) + 0.004));
  gCore.push(buildTube(spiCurve, 115, 5, (t) => 0.010 * (1 - 0.6 * t) + 0.002));

  /* 触角:11 节链式关节 */
  const ant = [];
  for (const side of [-1, 1]) {
    const segRoot = new Group();
    segRoot.position.set(side * 0.11, 0.168, -0.148);
    segRoot.rotation.set(-0.42, side * 0.30, side * 0.26);
    const gA = [];
    gA.push(placeGeo(new SphereGeometry(0.055, 14, 11), [0, 0, 0]));
    gA.push(placeGeo(new TorusGeometry(0.059, 0.013, 6, 18), [0, 0, 0], [Math.PI / 2, 0, 0]));
    const baseM = mergeTo(gA, MAT.dark);
    if (baseM) segRoot.add(baseM);

    const chain = [];
    let cur = segRoot;
    const N = 11;
    for (let i = 0; i < N; i++) {
      const link = new Group();
      const len = 0.148 - i * 0.006;
      link.position.set(0, i === 0 ? 0.038 : len, 0);
      link.rotation.z = side * (0.052 + i * 0.011);
      link.rotation.x = -0.018 - i * 0.006;
      const gr = [];
      gr.push(placeGeo(new CylinderGeometry(0.028 - i * 0.0013, 0.034 - i * 0.0013, len, 10), [0, len / 2, 0]));
      gr.push(placeGeo(new TorusGeometry(0.032 - i * 0.0013, 0.0075, 6, 14), [0, len, 0], [Math.PI / 2, 0, 0]));
      if (i % 3 === 0) gr.push(placeGeo(new SphereGeometry(0.040 - i * 0.0015, 10, 8), [0, len * 0.5, 0]));
      const m = mergeTo(gr, i % 2 ? MAT.metal : MAT.copper);
      if (m) link.add(m);
      chain.push(link);
      cur.add(link);
      cur = link;
    }
    /* 末端棒状感音器 */
    const club = new Group();
    club.position.set(0, 0.095, 0);
    const cg = [];
    for (let i = 0; i < 5; i++) cg.push(placeGeo(new SphereGeometry(0.050 - i * 0.006, 14, 11), [0, i * 0.050, 0]));
    const clubMesh = mergeTo(cg, MAT.metalBright);
    if (clubMesh) club.add(clubMesh);
    const clubCore = new Mesh(new SphereGeometry(0.026, 12, 10), MAT.core);
    clubCore.position.set(0, 0.228, 0);
    clubCore.userData.noXray = true;
    club.add(clubCore);
    const sp = new Sprite(MAT.glowSprite.clone());
    sp.scale.setScalar(0.38);
    sp.position.set(0, 0.228, 0);
    club.add(sp);
    lights.push(sp);
    cur.add(club);
    g.add(segRoot);
    ant.push({ root: segRoot, chain, club, core: clubCore, light: sp, side });
  }

  const add = (geo, mat, cast = true, recv = true) => {
    const m = mergeTo(geo, mat, cast, recv);
    if (m) g.add(m);
    return m;
  };
  add(gMetal, MAT.metal);
  add(gDark, MAT.dark);
  const cm = add(gCore, MAT.core, false, false);
  if (cm) { cm.renderOrder = 2; cm.userData.noXray = true; }

  g.position.set(0, 0.045, -1.18);
  g.userData.eyes = eyes;
  g.userData.antennae = ant;
  g.userData.lights = lights;
  return g;
}

function buildLeg(side) {
  const root = new Group();
  const coxa = new Group(), femur = new Group(), tibia = new Group(), tarsus = new Group();

  const seg = (parent, len, r0, r1, mat, detail) => {
    const g = [];
    g.push(placeGeo(new CylinderGeometry(r1, r0, len, 12), [0, -len / 2, 0]));
    g.push(placeGeo(new TorusGeometry(r0 * 1.06, 0.017, 6, 16), [0, 0, 0], [Math.PI / 2, 0, 0]));
    g.push(placeGeo(new TorusGeometry(r1 * 1.06, 0.015, 6, 14), [0, -len, 0], [Math.PI / 2, 0, 0]));
    if (detail === 'spike') {
      for (let i = 0; i < 5; i++) {
        const y = -len * (0.22 + i * 0.16);
        for (const s of [-1, 1]) g.push(placeGeo(new ConeGeometry(0.019, 0.080, 6),
          [s * r1 * 0.9, y, 0], [0, 0, -s * Math.PI / 2.6]));
      }
    }
    if (detail === 'piston') g.push(placeGeo(new CylinderGeometry(0.021, 0.021, len * 0.70, 8), [r0 * 0.70, -len * 0.42, 0]));
    const m = mergeTo(g, mat);
    if (m) parent.add(m);
    return m;
  };

  seg(coxa, 0.29, 0.060, 0.048, MAT.metal);
  const j0 = ball(0.070, MAT.metalBright, 14);
  coxa.add(j0);
  femur.position.set(0, -0.29, 0);
  seg(femur, 0.60, 0.056, 0.038, MAT.metal, 'piston');
  femur.add(ball(0.056, MAT.dark, 12));
  tibia.position.set(0, -0.60, 0);
  seg(tibia, 0.64, 0.038, 0.023, MAT.copper, 'spike');
  tibia.add(ball(0.042, MAT.metalBright, 12));
  tarsus.position.set(0, -0.64, 0);
  const tg = [];
  for (let i = 0; i < 4; i++) {
    tg.push(placeGeo(new CylinderGeometry(0.019 - i * 0.002, 0.021 - i * 0.002, 0.095, 8), [0, -0.048 - i * 0.093, 0]));
    tg.push(placeGeo(new SphereGeometry(0.022 - i * 0.002, 8, 7), [0, -0.095 - i * 0.093, 0]));
  }
  const tm = mergeTo(tg, MAT.dark);
  if (tm) tarsus.add(tm);
  for (const s of [-1, 1]) {
    const cm2 = mergeTo([buildTube(new CatmullRomCurve3([
      new Vector3(0, -0.38, 0), new Vector3(s * 0.043, -0.45, 0.01),
      new Vector3(s * 0.072, -0.42, 0.0)
    ], false, 'catmullrom', 0.4), 16, 6, (t) => 0.015 * (1 - 0.6 * t) + 0.003)], MAT.metalBright);
    if (cm2) tarsus.add(cm2);
  }
  coxa.add(femur); femur.add(tibia); tibia.add(tarsus);
  root.add(coxa);
  root.userData.parts = { coxa, femur, tibia, tarsus };
  return root;
}

function buildLegs(parent) {
  const legs = [];
  const cfg = [
    { z: -0.88, x: 0.33, rot: [0.50, 0.34, -0.30], scale: 0.80, group: 'fore' },
    { z: -0.28, x: 0.41, rot: [0.10, 0.06, -0.14], scale: 1.0, group: 'mid' },
    { z: 0.26, x: 0.37, rot: [-0.42, -0.30, 0.10], scale: 0.93, group: 'hind' }
  ];
  cfg.forEach((c, i) => {
    for (const side of [-1, 1]) {
      const leg = buildLeg(side);
      leg.position.set(side * c.x, -0.22, c.z);
      leg.rotation.set(c.rot[0], side * c.rot[1], side * c.rot[2]);
      leg.scale.setScalar(c.scale);
      leg.userData.cfg = { ...c, side, index: i };
      legs.push(leg);
      parent.add(leg);
    }
  });
  return legs;
}

/* SECTION: butterfly-assembly */
step('正在组装整机与传动轮系…');
const butterfly = new Group();
butterfly.name = 'butterfly';

const body = new Group();
body.name = 'body';
const thorax = buildThorax();
const abdomen = buildAbdomen();
const head = buildHead();
const legs = buildLegs(body);
body.add(thorax, abdomen, head);
butterfly.add(body);

const wingL = buildWingAssembly();
wingL.root.scale.x = -1;             /* 镜像得到左翅 */
wingL.root.position.set(-0.30, 0.20, -0.30);
const wingR = buildWingAssembly();
wingR.root.position.set(0.30, 0.20, -0.30);
butterfly.add(wingL.root, wingR.root);

wingL.root.add(wingLightL);
wingR.root.add(wingLightR);
wingLightL.position.copy(wingL.anchorFore);
wingLightR.position.copy(wingR.anchorFore);

const rig = {
  butterfly, body, thorax, abdomen, head, legs,
  wings: [wingL, wingR],
  gears: [...thorax.userData.gears, ...wingL.parts, ...wingR.parts],
  pistons: thorax.userData.pistons,
  vanes: [...wingL.vanes, ...wingR.vanes],
  segs: abdomen.userData.segs,
  antennae: head.userData.antennae,
  eyes: head.userData.eyes,
  coreBall: thorax.userData.coreBall,
  coreShell: thorax.userData.coreShell,
  lights: [...(head.userData.lights || []), ...wingL.lights, ...wingR.lights,
    ...(thorax.userData.lights || []), abdomen.userData.tailGlow].filter(Boolean)
};
butterfly.position.set(0, 2.55, 0);
scene.add(butterfly);

/* 整机拆解基准位(用于拆解视图) */
const EXPLODE = new Map();
function recordExplode(obj, dir, dist) {
  EXPLODE.set(obj, { base: obj.position.clone(), dir: dir.clone().normalize(), dist });
}
recordExplode(head, new Vector3(0, 0.4, -1), 1.15);
recordExplode(abdomen, new Vector3(0, 0.15, 1), 1.0);
recordExplode(wingL.root, new Vector3(-1, 0.35, 0), 1.5);
recordExplode(wingR.root, new Vector3(1, 0.35, 0), 1.5);
rig.legs.forEach((l, i) => recordExplode(l, new Vector3(i % 2 ? 1 : -1, -1, (i % 3) - 1).normalize(), 0.72));
/* 腹节为嵌套链,拆解位移会逐节累加,故用恒定间距得到线性展开 */
rig.segs.forEach((s) => recordExplode(s, new Vector3(0, 1, 0.22), 0.155));

/* SECTION: base — 齿轮基座与承力柱 */
step('正在构建齿轮基座…');
const baseGroup = new Group();
baseGroup.name = 'base';

/* CylinderGeometry 材质分组:0 侧面 / 1 顶面 / 2 底面 */
const plate = new Mesh(new CylinderGeometry(5.4, 5.55, 0.34, 96, 1),
  [MAT.plateSide, MAT.plate, MAT.plateSide]);
plate.position.y = -0.17;
plate.receiveShadow = true;
baseGroup.add(plate);

/* 基座外圈轮齿 */
const outerTeeth = [];
for (let i = 0; i < 60; i++) {
  const a = (i / 60) * Math.PI * 2;
  outerTeeth.push(placeGeo(new BoxGeometry(0.20, 0.24, 0.30),
    [Math.cos(a) * 5.52, -0.14, Math.sin(a) * 5.52], [0, -a, 0]));
}
const teethMesh = mergeTo(outerTeeth, MAT.metalDark);
if (teethMesh) baseGroup.add(teethMesh);

/* 承力柱与轴承座 */
const columns = [];
for (let i = 0; i < 4; i++) {
  const a = (i / 4) * Math.PI * 2 + Math.PI / 4;
  const cx = Math.cos(a) * 2.55, cz = Math.sin(a) * 2.55;
  const col = new Group();
  const cg = [];
  cg.push(placeGeo(new CylinderGeometry(0.15, 0.21, 2.35, 14), [0, 1.17, 0]));
  cg.push(placeGeo(new CylinderGeometry(0.26, 0.30, 0.16, 16), [0, 0.08, 0]));
  cg.push(placeGeo(new TorusGeometry(0.165, 0.026, 7, 20), [0, 0.55, 0], [Math.PI / 2, 0, 0]));
  cg.push(placeGeo(new TorusGeometry(0.165, 0.026, 7, 20), [0, 1.45, 0], [Math.PI / 2, 0, 0]));
  cg.push(placeGeo(new CylinderGeometry(0.24, 0.19, 0.20, 16), [0, 2.44, 0]));
  const cm = mergeTo(cg, MAT.metal);
  if (cm) col.add(cm);
  /* 柱顶发光环 */
  const ringGlow = new Mesh(new TorusGeometry(0.22, 0.022, 8, 26), MAT.core);
  ringGlow.position.y = 2.52;
  ringGlow.rotation.x = Math.PI / 2;
  ringGlow.userData.noXray = true;
  ringGlow.userData.anim = { type: 'pulse', phase: i * 1.3 };
  col.add(ringGlow);
  rig.lights.push(ringGlow);
  col.position.set(cx, 0, cz);
  columns.push(col);
  baseGroup.add(col);
}

/* 中央驱动齿轮组 */
const baseGears = [];
const gearSpecs = [
  { r: 1.45, t: 42, th: 0.20, x: 0, z: 0, sp: 0.42, mat: MAT.metal, spokes: 6 },
  { r: 0.72, t: 22, th: 0.16, x: 2.05, z: 0.55, sp: -0.85, mat: MAT.copper, spokes: 5 },
  { r: 0.55, t: 17, th: 0.14, x: -1.72, z: 1.42, sp: 1.12, mat: MAT.metalBright, spokes: 4 },
  { r: 0.88, t: 26, th: 0.18, x: -1.10, z: -2.05, sp: -0.70, mat: MAT.metal, spokes: 5 },
  { r: 0.46, t: 14, th: 0.13, x: 1.35, z: -2.28, sp: 1.60, mat: MAT.copper, spokes: 0 },
  { r: 0.62, t: 19, th: 0.15, x: 2.35, z: -1.30, sp: -0.98, mat: MAT.metalDark, spokes: 5 },
  { r: 0.40, t: 12, th: 0.12, x: -2.72, z: -0.62, sp: 1.90, mat: MAT.metalBright, spokes: 0 }
];
gearSpecs.forEach((s, i) => {
  const gm = gearMesh(s.r, s.t, s.th, s.mat, { bore: s.r * 0.16, spokes: s.spokes });
  gm.rotation.x = Math.PI / 2;
  gm.position.set(s.x, 0.20 + s.th * 0.5, s.z);
  gm.userData.anim = { type: 'spinLocalY', speed: s.sp };
  gm.userData.baseSpin = s.sp;
  baseGears.push(gm);
  baseGroup.add(gm);
  /* 轴与轴承 */
  const axle = new Mesh(new CylinderGeometry(s.r * 0.075, s.r * 0.09, 0.62, 12), MAT.metalBright);
  axle.position.set(s.x, 0.30, s.z);
  axle.castShadow = true;
  baseGroup.add(axle);
  const cap = new Mesh(new SphereGeometry(s.r * 0.10, 12, 9), MAT.core);
  cap.position.set(s.x, 0.62, s.z);
  cap.userData.noXray = true;
  cap.userData.anim = { type: 'pulse', phase: i * 0.7 };
  baseGroup.add(cap);
  rig.lights.push(cap);
});

/* 基座环形导轨与刻度 */
const railGeo = new TorusGeometry(3.85, 0.055, 8, 120);
const rail = new Mesh(railGeo, MAT.metalBright);
rail.rotation.x = Math.PI / 2;
rail.position.y = 0.10;
rail.castShadow = true;
baseGroup.add(rail);
const rail2 = new Mesh(new TorusGeometry(4.65, 0.035, 7, 120), MAT.metalDark);
rail2.rotation.x = Math.PI / 2;
rail2.position.y = 0.08;
baseGroup.add(rail2);

/* 导轨上的游标滑块(随蝴蝶环绕移动) */
const slider = new Group();
const sg = [];
sg.push(placeGeo(new BoxGeometry(0.34, 0.18, 0.22), [0, 0, 0]));
sg.push(placeGeo(new CylinderGeometry(0.05, 0.05, 0.30, 10), [0, 0.14, 0], [Math.PI / 2, 0, 0]));
const sliderMesh = mergeTo(sg, MAT.copper);
if (sliderMesh) slider.add(sliderMesh);
const sliderGlow = new Mesh(new SphereGeometry(0.055, 12, 9), MAT.core);
sliderGlow.position.y = 0.14;
sliderGlow.userData.noXray = true;
slider.add(sliderGlow);
rig.lights.push(sliderGlow);
slider.position.set(3.85, 0.16, 0);
baseGroup.add(slider);

/* 地面光池 */
const pool = new Sprite(new SpriteMaterial({
  map: TEX_POOL, color: T().accent, transparent: true, opacity: 0.34,
  blending: AdditiveBlending, depthWrite: false, toneMapped: false
}));
pool.scale.set(15, 15, 1);
pool.position.set(0, 0.06, 0);
pool.material.rotation = 0;
pool.userData.noXray = true;
baseGroup.add(pool);

/* 阴影承接面 */
const shadowCatcher = new Mesh(
  new CircleGeometry(9, 64),
  new THREE.ShadowMaterial({ opacity: 0.42 })
);
shadowCatcher.rotation.x = -Math.PI / 2;
shadowCatcher.position.y = 0.02;
shadowCatcher.receiveShadow = true;
shadowCatcher.userData.noXray = true;
baseGroup.add(shadowCatcher);

scene.add(baseGroup);
rig.baseGears = baseGears;
rig.slider = slider;
rig.pool = pool;

/* SECTION: particles */
step('正在生成悬浮微粒…');
const PCOUNT = isTouch ? 700 : 1500;
const pPos = new Float32Array(PCOUNT * 3);
const pSeed = new Float32Array(PCOUNT * 3);
const pSize = new Float32Array(PCOUNT);
for (let i = 0; i < PCOUNT; i++) {
  const r = 1.2 + Math.pow(Math.random(), 0.6) * 11;
  const a = Math.random() * Math.PI * 2;
  pPos[i * 3] = Math.cos(a) * r;
  pPos[i * 3 + 1] = Math.random() * 9.5 - 0.4;
  pPos[i * 3 + 2] = Math.sin(a) * r;
  pSeed[i * 3] = Math.random() * 100;
  pSeed[i * 3 + 1] = 0.25 + Math.random() * 0.85;
  pSeed[i * 3 + 2] = Math.random() * 100;
  pSize[i] = 6 + Math.random() * 22;
}
const pGeo = new BufferGeometry();
pGeo.setAttribute('position', new BufferAttribute(pPos, 3));
pGeo.setAttribute('aSeed', new BufferAttribute(pSeed, 3));
pGeo.setAttribute('aSize', new BufferAttribute(pSize, 1));
const pUniforms = {
  uTime: { value: 0 },
  uPR: { value: renderer.getPixelRatio() },
  uColorA: { value: new Color(T().accent) },
  uColorB: { value: new Color(T().accent2) },
  uOpacity: { value: 0.9 },
  uMap: { value: TEX_GLOW }
};
const particles = new Points(pGeo, new ShaderMaterial({
  uniforms: pUniforms, transparent: true, depthWrite: false,
  blending: AdditiveBlending, fog: false,
  vertexShader: `
    attribute vec3 aSeed; attribute float aSize;
    uniform float uTime, uPR;
    varying float vGlow; varying float vMix;
    void main(){
      vec3 p = position;
      float t = uTime * aSeed.y;
      p.x += sin(t * 0.34 + aSeed.x) * 0.55 + cos(t * 0.13 + aSeed.z) * 0.3;
      p.y += sin(t * 0.22 + aSeed.z * 1.7) * 0.75;
      p.z += cos(t * 0.29 + aSeed.x * 1.3) * 0.55;
      p.y = mod(p.y + 9.0, 11.0) - 1.0;
      vec4 mv = modelViewMatrix * vec4(p, 1.0);
      float dist = -mv.z;
      vGlow = 0.42 + 0.58 * (0.5 + 0.5 * sin(t * 1.7 + aSeed.x * 3.1));
      vMix = fract(aSeed.x * 0.137 + aSeed.z * 0.029);
      gl_PointSize = aSize * uPR * (13.0 / max(0.4, dist));
      gl_Position = projectionMatrix * mv;
    }`,
  fragmentShader: `
    uniform vec3 uColorA, uColorB; uniform float uOpacity; uniform sampler2D uMap;
    varying float vGlow; varying float vMix;
    void main(){
      vec4 t = texture2D(uMap, gl_PointCoord);
      vec3 c = mix(uColorA, uColorB, vMix);
      gl_FragColor = vec4(c * (0.6 + vGlow * 1.5), t.a * vGlow * uOpacity);
      if (gl_FragColor.a < 0.004) discard;
    }`
}));
particles.frustumCulled = false;
particles.userData.noXray = true;
scene.add(particles);

/* 近景光晕精灵(体积感) */
const hazeSprites = [];
for (let i = 0; i < 7; i++) {
  const s = new Sprite(new SpriteMaterial({
    map: TEX_GLOW, color: i % 2 ? T().accent2 : T().accent, transparent: true,
    opacity: 0.10, blending: AdditiveBlending, depthWrite: false, toneMapped: false
  }));
  const a = (i / 7) * Math.PI * 2;
  const r = 5.2 + Math.random() * 3.4;
  s.position.set(Math.cos(a) * r, 1.2 + Math.random() * 5.5, Math.sin(a) * r);
  s.scale.setScalar(5 + Math.random() * 5);
  s.userData.noXray = true;
  s.userData.base = s.position.clone();
  s.userData.phase = Math.random() * 10;
  hazeSprites.push(s);
  scene.add(s);
}
rig.haze = hazeSprites;

/* SECTION: postprocessing */
step('正在接入辉光后期…');
const composer = new EffectComposer(renderer);
composer.setPixelRatio(pixelRatio);
composer.setSize(window.innerWidth, window.innerHeight);
const renderPass = new RenderPass(scene, camera);
const bloomPass = new UnrealBloomPass(
  new Vector2(window.innerWidth, window.innerHeight), 0.85, 0.72, 0.62);
composer.addPass(renderPass);
composer.addPass(bloomPass);
composer.addPass(new OutputPass());

/* SECTION: state */
const state = {
  mode: 'hover',
  freq: 1.0,
  bloom: 0.85,
  autoRotate: true,
  xray: false,
  explode: false,
  hotspots: true,
  paused: false,
  wingPhase: 0,
  time: 0,
  explodeAmt: 0,
  xrayAmt: 0,
  fps: 0
};

/* 飞行轨迹:三维利萨如曲线 */
const flightCurve = new CatmullRomCurve3([
  new Vector3(0, 3.30, 0), new Vector3(3.30, 3.85, 2.10),
  new Vector3(4.60, 2.95, -1.30), new Vector3(1.60, 3.55, -4.20),
  new Vector3(-2.30, 4.15, -3.10), new Vector3(-4.80, 3.20, 0.60),
  new Vector3(-3.20, 2.75, 3.40), new Vector3(0.40, 3.60, 4.30)
], true, 'catmullrom', 0.5);
let flightT = 0;

/* SECTION: animation */
const _v1 = new Vector3(), _v2 = new Vector3(), _v3 = new Vector3(), _q = new Quaternion();
function animateButterfly(dt) {
  const t = state.time;
  const f = state.freq;
  const ph = state.wingPhase;

  /* --- 扑翼:非正弦机构曲线(下压快、上抬慢,末端带弹性回冲) --- */
  const s = Math.sin(ph);
  const asym = s >= 0 ? Math.pow(s, 0.72) : -Math.pow(-s, 1.28);
  const flap = asym * 0.66 + Math.sin(ph * 2) * 0.055;
  const feather = Math.sin(ph + Math.PI * 0.42) * 0.30;

  for (const w of rig.wings) {
    /* 左翅已由 root.scale.x = -1 完成几何镜像;扑翼旋转左右取同号,
       才能得到镜像对称的同步运动(否则一上一下)。 */
    w.flap.rotation.z = flap;
    w.flap.rotation.x = feather * 0.42;
    w.flap.rotation.y = 0.06 + Math.sin(ph + 1.1) * 0.10;
    /* 后翅相位滞后,形成波浪传递 */
    w.hindPivot.rotation.z = Math.sin(ph - 0.52) * 0.14;
    w.hindPivot.rotation.x = Math.sin(ph - 0.3) * 0.07;
    /* 翼面柔性形变 */
    const bend = Math.cos(ph) * 0.085;
    w.forePivot.rotation.y = bend * 0.55;
    w.hindPivot.rotation.y = -0.13 + bend * 0.32;
  }

  /* --- 齿轮轮系:与扑翼频率严格联动 --- */
  const gearSpeed = flap * 2.6;
  for (const g of rig.gears) {
    const a = g.userData.anim;
    if (!a) continue;
    if (a.type === 'spin') g.rotation.z += dt * a.speed * (0.6 + Math.abs(gearSpeed));
  }
  for (const g of rig.baseGears) {
    g.rotation.z += dt * g.userData.baseSpin * (0.55 + Math.abs(gearSpeed) * 0.55);
  }
  for (const g of thorax.userData.gears) {
    g.rotation.z += dt * 3.0 * (0.6 + Math.abs(gearSpeed));
  }

  /* --- 活塞:随扑翼往复 --- */
  rig.pistons.forEach((p, i) => {
    const stroke = Math.sin(ph + (i ? Math.PI : 0)) * p.userData.anim.amp;
    p.scale.y = 1 + stroke * 0.55;
    p.position.y = 0.15 - stroke * 0.16;
  });

  /* --- 腹节波浪式摆动(链式旋转,位置交由拆解逻辑独占) --- */
  rig.segs.forEach((seg, i) => {
    const w = Math.sin(ph * 0.5 - i * 0.52);
    const w2 = Math.sin(t * 0.85 - i * 0.40);
    seg.rotation.x = w * 0.055 + w2 * 0.018;
    seg.rotation.y = Math.sin(t * 0.62 - i * 0.44) * 0.050;
    seg.rotation.z = Math.cos(t * 0.5 - i * 0.3) * 0.028;
  });

  /* --- 尾翼可调扇叶 --- */
  rig.vanes.forEach((v) => {
    const a = v.userData.anim;
    v.rotation.z = Math.sin(t * a.speed + a.phase) * a.amp + Math.sin(ph) * 0.10;
    v.rotation.x = Math.cos(t * a.speed * 0.7 + a.phase) * a.amp * 0.4;
  });

  /* --- 触角:链式延迟摆动 --- */
  rig.antennae.forEach((ant) => {
    const sway = Math.sin(t * 1.35) * 0.10 + Math.sin(ph * 0.5) * 0.05;
    ant.root.rotation.x = -0.42 + sway * 0.55 + Math.sin(t * 2.1) * 0.035;
    ant.root.rotation.z = ant.side * (0.26 + Math.sin(t * 1.1 + 1.2) * 0.075);
    ant.chain.forEach((link, i) => {
      const d = i * 0.34;
      link.rotation.x = -0.018 - i * 0.006 + Math.sin(t * 2.3 - d) * 0.052 * (1 + i * 0.11);
      link.rotation.z = ant.side * (0.052 + i * 0.011) + Math.cos(t * 1.85 - d) * 0.044;
    });
    ant.club.rotation.z = Math.sin(t * 3.1) * 0.13;
  });

  /* --- 复眼与核心呼吸 --- */
  const breathe = 0.5 + 0.5 * Math.sin(t * 2.2);
  MAT.eye.emissiveIntensity = 1.15 + breathe * 1.05;
  rig.coreBall.scale.setScalar(1 + breathe * 0.10);
  rig.coreBall.rotation.x += dt * 0.9;
  rig.coreBall.rotation.y += dt * 1.35;
  MAT.core.color.copy(themeCur.core).multiplyScalar(0.82 + breathe * 0.55);
  coreLight.intensity = themeNum.corePower * (0.72 + breathe * 0.62);
  rig.coreShell.scale.setScalar(1 + breathe * 0.035);

  /* --- 发光元件强度 --- */
  rig.lights.forEach((l, i) => {
    if (!l.material) return;
    const k = 0.42 + 0.58 * (0.5 + 0.5 * Math.sin(t * (1.6 + (i % 5) * 0.35) + i * 1.7));
    l.material.opacity = 0.28 + k * 0.62;
    if (l.isSprite) {
      const sc = l.userData.baseScale || (l.userData.baseScale = l.scale.x);
      l.scale.setScalar(sc * (0.82 + k * 0.42));
    }
  });

  /* --- 六足:抓握姿态微动 --- */
  rig.legs.forEach((leg) => {
    const c = leg.userData.cfg;
    const p = leg.userData.parts;
    const off = c.index * 1.9 + (c.side > 0 ? 0 : 1.0);
    const idle = Math.sin(t * 0.9 + off) * 0.055;
    const pump = Math.sin(ph * 0.5 + off) * 0.075 * (state.mode === 'hover' ? 1 : 0.35);
    p.coxa.rotation.x = c.rot[0] * 0.10 + idle;
    p.coxa.rotation.z = c.side * (0.10 + pump * 0.4);
    p.femur.rotation.x = -0.28 + idle * 1.4 + pump;
    p.femur.rotation.z = c.side * (0.16 - pump * 0.5);
    p.tibia.rotation.x = 0.72 + Math.sin(t * 1.2 + off) * 0.10 - pump * 1.3;
    p.tarsus.rotation.x = -0.34 + Math.sin(t * 1.6 + off) * 0.13;
  });

  /* --- 翅尖灯光位置 --- */
  wingLightL.intensity = wingLightR.intensity = 1.1 + breathe * 1.1;
}

/* --- 三种飞行模式 --- */
const hoverAnchor = new Vector3(0, 2.55, 0);
function animateFlight(dt) {
  const t = state.time;
  const b = rig.butterfly;
  if (state.mode === 'display') {
    /* 展翼陈列:缓慢开合,机身稳定,基座齿轮慢转 */
    b.position.lerp(_v1.set(0, 2.42 + Math.sin(t * 0.55) * 0.07, 0), 0.08);
    b.rotation.x += (Math.sin(t * 0.4) * 0.05 - b.rotation.x) * 0.06;
    b.rotation.y += (Math.sin(t * 0.22) * 0.30 - b.rotation.y) * 0.05;
    b.rotation.z += (Math.sin(t * 0.33) * 0.03 - b.rotation.z) * 0.06;
    camState.targetGoal.set(0, 2.45, 0);
    state.wingPhase += dt * state.freq * 1.05;
  } else if (state.mode === 'orbit') {
    /* 环绕飞行:沿三维曲线巡航并压坡度 */
    flightT = (flightT + dt * 0.035 * (0.6 + state.freq * 0.5)) % 1;
    const pos = flightCurve.getPointAt(flightT);
    const ahead = flightCurve.getPointAt((flightT + 0.012) % 1);
    b.position.lerp(pos, 0.14);
    const dir = _v2.subVectors(ahead, pos).normalize();
    const yaw = Math.atan2(dir.x, dir.z);
    const pitch = -Math.asin(MathUtils.clamp(dir.y, -1, 1));
    /* 曲率 → 坡度 */
    const ahead2 = flightCurve.getPointAt((flightT + 0.05) % 1);
    const d2 = _v3.subVectors(ahead2, ahead).normalize();
    const turn = Math.atan2(d2.x, d2.z) - yaw;
    const bank = -MathUtils.clamp(Math.atan2(Math.sin(turn), Math.cos(turn)) * 5.5, -0.85, 0.85);
    _q.setFromEuler(new Euler(pitch * 0.85, yaw, bank, 'YXZ'));
    b.quaternion.slerp(_q, 0.075);
    b.position.y += Math.sin(t * 2.1) * 0.05;
    camState.targetGoal.lerp(b.position, 0.10);
    state.wingPhase += dt * state.freq * 6.6;
  } else {
    /* 悬停:定点悬浮 + 高频振翅 + 微幅漂移 */
    const drift = _v1.set(
      Math.sin(t * 0.62) * 0.30 + Math.sin(t * 1.31) * 0.11,
      2.62 + Math.sin(t * 0.95) * 0.26 + Math.sin(t * 2.2) * 0.07,
      Math.cos(t * 0.53) * 0.28 + Math.cos(t * 1.17) * 0.10
    );
    b.position.lerp(drift, 0.09);
    b.rotation.x += (Math.sin(t * 1.1) * 0.075 + Math.sin(ph2()) * 0.035 - b.rotation.x) * 0.09;
    b.rotation.z += (Math.cos(t * 0.85) * 0.06 - b.rotation.z) * 0.08;
    b.rotation.y += (Math.sin(t * 0.30) * 0.42 - b.rotation.y) * 0.045;
    camState.targetGoal.lerp(_v2.set(b.position.x * 0.35, 2.45 + b.position.y * 0.12, b.position.z * 0.35), 0.06);
    state.wingPhase += dt * state.freq * 8.4;
  }
  /* 基座游标跟随蝴蝶方位角 */
  const ang = Math.atan2(b.position.z, b.position.x);
  rig.slider.position.set(Math.cos(ang) * 3.85, 0.16, Math.sin(ang) * 3.85);
  rig.slider.rotation.y = -ang;
}
function ph2() { return state.wingPhase; }

/* --- 拆解 / 透视过渡 --- */
function animateStates(dt) {
  const kx = 1 - Math.pow(0.004, dt);
  state.xrayAmt += ((state.xray ? 1 : 0) - state.xrayAmt) * kx;
  state.explodeAmt += ((state.explode ? 1 : 0) - state.explodeAmt) * (1 - Math.pow(0.008, dt));
  if (state.explodeAmt > 0.0015) {
    const e = state.explodeAmt;
    EXPLODE.forEach((v, obj) => {
      obj.position.copy(v.base).addScaledVector(v.dir, v.dist * e);
    });
  } else if (state.explodeAmt <= 0.0015 && !state.explode) {
    EXPLODE.forEach((v, obj) => obj.position.copy(v.base));
  }
  MAT.xray.opacity = 0.16 + state.xrayAmt * 0.44;
  MAT.xraySoft.opacity = 0.05 + state.xrayAmt * 0.16;
}

/* SECTION: hotspots */
step('正在标注机械部件…');
const HOTSPOTS = [
  { id: 'eye', name: '复眼光学阵列', key: 'COMPOUND EYE ARRAY', target: () => rig.head.userData.eyes[1],
    desc: '由 6,200 枚六边形小眼面组成的晶格透镜阵列,每枚小眼后接一组三级聚光筒。表面覆盖增透镀膜,可在低照度下维持成像,并为主控轮系提供姿态与光流参考。' },
  { id: 'antenna', name: '链节式触须天线', key: 'ARTICULATED ANTENNA', target: () => rig.antennae[1].club,
    desc: '十一节球铰链节结构,每节内置微型阻尼器,可被动感知气流扰动。末端棒状感音器内嵌谐振腔,用于检测同型机体的振翅频率以完成编队同步。' },
  { id: 'core', name: '胸腔反应核心', key: 'THORACIC CORE', target: () => rig.coreBall,
    desc: '整机动力源。二十面体能量体悬浮于透光壳体中,外层九道黄铜环构成磁约束骨架。输出功率经胸甲内齿轮箱分流至四组扑翼曲柄与六足伺服机构。' },
  { id: 'gearbox', name: '翅根减速齿轮箱', key: 'WING-ROOT GEARBOX', target: () => rig.thorax.userData.gears[0],
    desc: '二十齿斜齿轮组,将核心高转速输出降至扑翼所需的低速大扭矩。齿面经硬化处理,啮合处设有油膜腔,是整机传动效率最关键的一环。' },
  { id: 'wingFore', name: '前翅桁架与翅室', key: 'FOREWING TRUSS', target: () => wingR.forePivot,
    desc: '九根主翅脉构成变截面桁架,脉内贯穿发光导管;翅脉之间以三向横撑加固,围合出多个琉璃翅室。翅室采用双层结构:外层透光琉璃承担气动载荷,内层发光膜负责虹彩显色。' },
  { id: 'wingHind', name: '后翅与可调扇叶', key: 'HINDWING & VANE', target: () => rig.vanes[2],
    desc: '后翅相位滞后于前翅约三十度,形成波浪式升力传递。后缘五片可调扇叶独立俯仰,配合两条尾突桨叶,用于低速悬停时的姿态微调与瞬间转向。' },
  { id: 'piston', name: '扑翼作动筒', key: 'FLAP ACTUATOR', target: () => rig.pistons[1],
    desc: '双作用活塞筒,行程与振翅频率严格同步。下压冲程快、回抬冲程慢的非对称运动曲线由凸轮配气机构决定,这是产生净升力的关键。' },
  { id: 'abdomen', name: '分节腹甲', key: 'SEGMENTED ABDOMEN', target: () => rig.segs[4],
    desc: '九节串联腹甲,每节由外圈金属环、背板与侧发光条组成,节间以波纹管衔接允许三维柔性弯曲。飞行时按正弦波逐节延迟摆动,等效于一条可主动调节的配平舵面。' },
  { id: 'leg', name: '六足关节组', key: 'HEXAPOD LEG ASSEMBLY', target: () => rig.legs[3],
    desc: '基节—腿节—胫节—四节跗节的四段式结构,胫节带五对倒刺棘用于攀附。落地时六足按三角步态交替支撑,各关节阻尼独立可调。' },
  { id: 'base', name: '基座驱动轮系', key: 'BASE GEAR TRAIN', target: () => rig.baseGears[1],
    desc: '陈列基座内的七组齿轮构成一级行星轮系,与胸甲齿轮箱同轴联动。导轨上的游标滑块实时指示机体方位角,兼作无线供能的耦合触点。' }
];
const hotLayer = $('hotspots');
const hsEls = HOTSPOTS.map((h) => {
  const wrap = document.createElement('div');
  wrap.className = 'hs';
  wrap.hidden = true;
  const btn = document.createElement('button');
  btn.type = 'button';
  btn.className = 'hs__dot';
  btn.setAttribute('aria-label', h.name);
  const lab = document.createElement('span');
  lab.className = 'hs__label';
  lab.textContent = h.name;
  wrap.appendChild(btn);
  wrap.appendChild(lab);
  hotLayer.appendChild(wrap);
  btn.addEventListener('click', (e) => { e.stopPropagation(); selectHotspot(h.id); });
  return { def: h, el: wrap, btn };
});

const infoEl = $('infocard');
let selId = null;
function selectHotspot(id) {
  const h = HOTSPOTS.find((x) => x.id === id);
  if (!h) return;
  selId = (selId === id) ? null : id;
  hsEls.forEach((e) => e.el.classList.toggle('is-sel', e.def.id === selId));
  if (!selId) { infoEl.hidden = true; return; }
  $('infoK').textContent = h.key;
  $('infoT').textContent = h.name;
  $('infoD').textContent = h.desc;
  infoEl.hidden = false;
}
$('infoX').addEventListener('click', () => {
  selId = null;
  hsEls.forEach((e) => e.el.classList.remove('is-sel'));
  infoEl.hidden = true;
});
/* 点击空白处取消选中 */
let downX = 0, downY = 0;
canvas.addEventListener('pointerdown', (e) => { downX = e.clientX; downY = e.clientY; });
canvas.addEventListener('click', (e) => {
  if (Math.abs(e.clientX - downX) + Math.abs(e.clientY - downY) > 6) return;
  if (selId) { selId = null; hsEls.forEach((x) => x.el.classList.remove('is-sel')); infoEl.hidden = true; }
});

const _proj = new Vector3();
const _camDir = new Vector3();
function updateHotspots() {
  const w = window.innerWidth, h = window.innerHeight;
  camera.getWorldDirection(_camDir);
  const visible = state.hotspots && !state.xray;
  for (const item of hsEls) {
    if (!visible) { if (!item.el.hidden) item.el.hidden = true; continue; }
    const obj = item.def.target();
    if (!obj) { item.el.hidden = true; continue; }
    obj.getWorldPosition(_proj);
    const toCam = _v1.subVectors(camera.position, _proj).normalize();
    _proj.project(camera);
    const facing = toCam.dot(_camDir) > 0;
    if (!facing || _proj.x < -1.08 || _proj.x > 1.08 || _proj.y < -1.08 || _proj.y > 1.08) {
      item.el.hidden = true; continue;
    }
    item.el.hidden = false;
    const sx = (_proj.x * 0.5 + 0.5) * w;
    const sy = (-_proj.y * 0.5 + 0.5) * h;
    item.el.style.transform = `translate3d(${sx.toFixed(1)}px,${sy.toFixed(1)}px,0)`;
    item.el.style.zIndex = String(2000 - Math.round(_proj.z * 900));
    item.el.style.opacity = String(MathUtils.clamp(1.25 - _proj.z * 0.9, 0.28, 1));
  }
}

/* SECTION: controls */
const modeSeg = $('modeSeg');
modeSeg.addEventListener('click', (e) => {
  const btn = e.target.closest('.seg__btn');
  if (!btn) return;
  setMode(btn.dataset.mode);
});
function setMode(m) {
  state.mode = m;
  [...modeSeg.children].forEach((b) => b.classList.toggle('is-on', b.dataset.mode === m));
  camState.shake = 0.05;
  if (m === 'orbit') camState.goal.radius = Math.max(camState.goal.radius, 13.5);
  if (m === 'display') { camState.goal.radius = 11.2; camState.goal.phi = 1.30; }
  if (m === 'hover') camState.targetGoal.set(0, 2.45, 0);
}
function bindRange(el, out, fmt, apply) {
  const upd = () => {
    const v = parseFloat(el.value);
    const pct = ((v - el.min) / (el.max - el.min)) * 100;
    el.style.setProperty('--fill', pct + '%');
    out.textContent = fmt(v);
    apply(v);
  };
  el.addEventListener('input', upd);
  upd();
}
bindRange($('freq'), $('freqOut'), (v) => v.toFixed(2) + '×', (v) => { state.freq = v; });
bindRange($('bloomS'), $('bloomOut'), (v) => v.toFixed(2), (v) => {
  state.bloom = v;
  bloomPass.strength = v;
});

$('themes').addEventListener('click', (e) => {
  const btn = e.target.closest('.theme');
  if (btn) applyTheme(btn.dataset.theme);
});

/* 视图开关 */
function bindToggle(el, key, onChange) {
  el.addEventListener('click', () => {
    state[key] = !state[key];
    el.classList.toggle('is-on', state[key]);
    el.setAttribute('aria-pressed', String(state[key]));
    if (onChange) onChange(state[key]);
  });
}
bindToggle($('tSpin'), 'autoRotate', (v) => { camState.autoRotate = v; });
bindToggle($('tXray'), 'xray', (v) => {
  setXray(v);
  if (v && selId) { selId = null; infoEl.hidden = true; hsEls.forEach((e) => e.el.classList.remove('is-sel')); }
});
bindToggle($('tExplode'), 'explode');
bindToggle($('tHot'), 'hotspots');
$('reset').addEventListener('click', () => {
  camState.reset();
  camState.shake = 0.05;
});

/* 移动端抽屉 */
const panel = $('panel'), panelToggle = $('panelToggle');
if (!matchMedia('(max-width:900px)').matches) panel.classList.remove('is-collapsed');
panelToggle.addEventListener('click', () => {
  const collapsed = panel.classList.toggle('is-collapsed');
  panelToggle.setAttribute('aria-expanded', String(!collapsed));
});

/* 键盘 */
addEventListener('keydown', (e) => {
  if (e.target.matches('input,textarea,select')) return;
  const k = e.key.toLowerCase();
  if (k === ' ' || e.code === 'Space') {
    e.preventDefault();
    state.paused = !state.paused;
  } else if (k === 'w') $('tXray').click();
  else if (k === 'e') $('tExplode').click();
  else if (k === 'r') { camState.reset(); camState.shake = 0.05; }
  else if (k === 'h') $('tHot').click();
});

/* 提示条自动淡出 */
setTimeout(() => $('hint').classList.add('is-gone'), 7200);

/* SECTION: resize */
function onResize() {
  const w = window.innerWidth, h = window.innerHeight;
  camera.aspect = w / h;
  camera.updateProjectionMatrix();
  renderer.setPixelRatio(pixelRatio);
  renderer.setSize(w, h, false);
  composer.setPixelRatio(pixelRatio);
  composer.setSize(w, h);
  bloomPass.setSize(w, h);
  pUniforms.uPR.value = renderer.getPixelRatio();
}
addEventListener('resize', onResize);

/* SECTION: loop */
step('正在启动渲染循环…');
const sFps = $('sFps'), sTri = $('sTri'), sCall = $('sCall');
let fpsAcc = 0, fpsN = 0, statTimer = 0;
let started = false;

function tick() {
  requestAnimationFrame(tick);
  const dt = Math.min(clock.getDelta(), 0.05);
  if (!state.paused) {
    state.time += dt;
    animateFlight(dt);
    animateButterfly(dt);
    bgUniforms.uTime.value = state.time;
    pUniforms.uTime.value = state.time;
  }
  animateStates(dt);
  updateTheme(dt);
  camState.update(dt);

  /* 悬浮微粒整体缓慢旋转 */
  particles.rotation.y += dt * 0.012;
  rig.haze.forEach((s, i) => {
    const b = s.userData.base;
    s.position.y = b.y + Math.sin(state.time * 0.22 + s.userData.phase) * 0.7;
    s.material.opacity = 0.06 + 0.05 * (0.5 + 0.5 * Math.sin(state.time * 0.4 + i));
  });

  updateHotspots();
  composer.render();

  /* 统计 */
  fpsAcc += dt; fpsN++;
  statTimer += dt;
  if (statTimer > 0.4) {
    state.fps = fpsN / fpsAcc;
    sFps.textContent = state.fps.toFixed(0);
    sTri.textContent = renderer.info.render.triangles.toLocaleString('en-US');
    sCall.textContent = renderer.info.render.calls;
    fpsAcc = 0; fpsN = 0; statTimer = 0;
    /* 自适应降级:低帧率时降低像素比 */
    if (!started) return;
    if (state.fps < 34 && pixelRatio > 1) {
      pixelRatio = Math.max(1, pixelRatio - 0.25);
      onResize();
    }
  }
}

/* SECTION: start */
collectSpriteMats();
applyThemeToScene();
applyTheme('brass');
themeMix = 1;
setMode('hover');
camState.reset();
onResize();
clock.getDelta();
renderer.compile(scene, camera);
composer.render();
tick();
started = true;

requestAnimationFrame(() => {
  ldFill.style.width = '100%';
  ldMsg.textContent = '构建完成';
  setTimeout(() => loaderEl.classList.add('is-done'), 320);
});

/* 上下文丢失保护 */
canvas.addEventListener('webglcontextlost', (e) => {
  e.preventDefault();
  loaderEl.classList.remove('is-done');
  fatal('渲染上下文已丢失', '显卡资源被系统回收。请刷新页面重新载入场景。');
});
</script>
</body>
</html>

三、MiMo-V2.6-Pro:极简快刀手,标准答案机器

168 行,12KB,单字母变量名,代码密度高得像压缩饼干。

官方全家桶安排得明明白白:OrbitControls、EffectComposer、UnrealBloomPass 一步到位。齿轮、复眼、触角、关节该有的都有,发光呼吸、粒子尘埃、自动环绕一样不少,打开就能跑。

不炫技,不铺摊子,用最少的代码达到 80 分效果。

一句话总结:性价比之王,交付界的闪电侠。

以下为MiMo-2.6Pro生成的机械蝴蝶代码
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Mechanical Butterfly</title>
<style>
body{margin:0;background:#0a0a15;overflow:hidden}canvas{display:block}
#t{position:fixed;top:20px;width:100%;text-align:center;color:#ffd70066;font:28px Georgia;letter-spacing:12px;pointer-events:none}
#i{position:fixed;bottom:14px;width:100%;text-align:center;color:#fff2;font:12px Georgia;pointer-events:none}
</style>
</head>
<body>
<div id="t">⚙ 机械蝴蝶 ⚙</div>
<div id="i">拖拽旋转 · 滚轮缩放</div>
<script type="importmap">
{"imports":{"three":"https://unpkg.com/three@0.160.0/build/three.module.js","three/addons/":"https://unpkg.com/three@0.160.0/examples/jsm/"}}
</script>
<script type="module">
import*as T 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{OutputPass}from'three/addons/postprocessing/OutputPass.js';

const S=new T.Scene();S.background=new T.Color(0x0a0a15);S.fog=new T.FogExp2(0x0a0a15,.02);
const C=new T.PerspectiveCamera(50,innerWidth/innerHeight,.1,100);C.position.set(3,2,5);
const R=new T.WebGLRenderer({antialias:true});R.setSize(innerWidth,innerHeight);
R.setPixelRatio(Math.min(devicePixelRatio,2));R.toneMapping=T.ACESFilmicToneMapping;
R.outputColorSpace=T.SRGBColorSpace;document.body.appendChild(R.domElement);
const O=new OrbitControls(C,R.domElement);O.enableDamping=true;O.autoRotate=true;O.autoRotateSpeed=.5;

const pm=new T.PMREMGenerator(R);const es=new T.Scene();
es.add(new T.Mesh(new T.SphereGeometry(50,32,32),new T.MeshBasicMaterial({color:0x121220,side:T.BackSide})));
[[0xffcc66,15,12,8,5],[0x4488ff,-12,10,-8,4],[0xff6600,5,-8,12,3],[0x8866ff,8,5,-15,2]].forEach(d=>{
  const m=new T.Mesh(new T.SphereGeometry(d[4],16,16),new T.MeshBasicMaterial({color:d[0]}));
  m.position.set(d[1],d[2],d[3]);es.add(m);});
S.environment=pm.fromScene(es,.04).texture;pm.dispose();

S.add(new T.AmbientLight(0x334455,.4));
const kl=new T.DirectionalLight(0xffcc88,2.5);kl.position.set(5,5,5);S.add(kl);
const fl=new T.DirectionalLight(0x4488cc,.7);fl.position.set(-5,3,-3);S.add(fl);
const rl=new T.PointLight(0xff5500,3,15);rl.position.set(-3,3,-4);S.add(rl);

const BR=new T.MeshStandardMaterial({color:0xb5a642,metalness:.95,roughness:.3,envMapIntensity:1.5,side:T.DoubleSide});
const CU=new T.MeshStandardMaterial({color:0xb87333,metalness:.9,roughness:.35,envMapIntensity:1.3});
const ST=new T.MeshStandardMaterial({color:0x909099,metalness:1,roughness:.15,envMapIntensity:1.8});
const DK=new T.MeshStandardMaterial({color:0x2a2a33,metalness:.9,roughness:.4,envMapIntensity:1});
const GL=new T.MeshPhysicalMaterial({color:0x88ccff,metalness:0,roughness:.05,transparent:true,opacity:.2,transmission:.8,envMapIntensity:2,side:T.DoubleSide});
const GD=new T.MeshStandardMaterial({color:0xffd700,metalness:1,roughness:.15,envMapIntensity:2});
const GO=new T.MeshStandardMaterial({color:0,emissive:0xff6600,emissiveIntensity:3});
const GB=new T.MeshStandardMaterial({color:0,emissive:0x00aaff,emissiveIntensity:2});

const gears=[];
function mkG(r,tk,n,sp){if(sp===void 0)sp=1;const g=new T.Group(),i=new T.Group();
  i.add(new T.Mesh(new T.CylinderGeometry(r,r,tk,32),BR));
  for(let j=0;j<n;j++){const a=j/n*Math.PI*2,o=new T.Mesh(new T.BoxGeometry(r*.18,tk*1.3,r*.12),BR);
    o.position.set(Math.cos(a)*(r+r*.07),0,Math.sin(a)*(r+r*.07));o.rotation.y=-a;i.add(o);}
  i.add(new T.Mesh(new T.CylinderGeometry(r*.2,r*.2,tk*1.5,12),ST));
  i.add(new T.Mesh(new T.CylinderGeometry(r*.06,r*.06,tk*2.2,8),DK));
  for(let j=0;j<4;j++){const s2=new T.Mesh(new T.BoxGeometry(r*1.6,tk*.6,r*.05),ST);s2.rotation.y=j/4*Math.PI;i.add(s2);}
  g.add(i);g.userData={sp:i,speed:sp};gears.push(g);return g;}
function bolt(p,s){if(s===void 0)s=.014;const m=new T.Mesh(new T.CylinderGeometry(s,s*.8,s*1.5,6),ST);m.position.copy(p);return m;}
function hC(s,cx,cy,r){const h=new T.Path(),N=12;
  for(let i=0;i<=N;i++){const a=i/N*Math.PI*2,x=cx+r*Math.cos(a),y=cy+r*Math.sin(a);
    i===0?h.moveTo(x,y):h.lineTo(x,y);}s.holes.push(h);}
function hR(s,x,y,w,h){const p=new T.Path();
  p.moveTo(x,y);p.lineTo(x+w,y);p.lineTo(x+w,y+h);p.lineTo(x,y+h);p.lineTo(x,y);s.holes.push(p);}

const B=new T.Group();S.add(B);

const hd=new T.Group();hd.position.set(0,.1,.6);
hd.add(new T.Mesh(new T.SphereGeometry(.15,24,24),BR));
[-1,1].forEach(function(s){const e=new T.Group();
  e.add(new T.Mesh(new T.TorusGeometry(.08,.015,16,24),ST));
  e.add(new T.Mesh(new T.SphereGeometry(.07,16,16),GL));
  const ir=new T.Mesh(new T.SphereGeometry(.03,12,12),s>0?GO:GB);ir.position.z=.03;e.add(ir);
  for(let i=0;i<6;i++){const a=i/6*Math.PI*2,f=new T.Mesh(new T.BoxGeometry(.02,.02,.01),GD);f.position.set(Math.cos(a)*.085,Math.sin(a)*.085,0);e.add(f);}
  e.position.set(s*.08,.04,.1);e.rotation.y=s*.3;hd.add(e);});
const hg=mkG(.04,.01,8,2);hg.position.set(0,.12,.05);hg.rotation.x=Math.PI/2;hd.add(hg);
const prob=new T.Mesh(new T.CylinderGeometry(.008,.02,.2,8),CU);prob.rotation.x=Math.PI/2;prob.position.set(0,-.05,.2);hd.add(prob);
[-1,1].forEach(function(s){const jg=mkG(.02,.008,6,s*3);jg.position.set(s*.04,-.08,.12);jg.rotation.x=Math.PI/2;hd.add(jg);});
[-1,1].forEach(function(s){const an=new T.Group();
  const cu=new T.CubicBezierCurve3(new T.Vector3(s*.05,.08,.05),new T.Vector3(s*.1,.25,.1),new T.Vector3(s*.15,.35,.15),new T.Vector3(s*.2,.4,.2));
  an.add(new T.Mesh(new T.TubeGeometry(cu,20,.008,6),CU));
  const tp=new T.Mesh(new T.SphereGeometry(.02,12,12),s>0?GO:GB);tp.position.set(s*.2,.4,.2);an.add(tp);
  for(let i=0;i<5;i++){const p=cu.getPoint(.1+i*.18),rg=new T.Mesh(new T.TorusGeometry(.01,.003,6,12),GD);rg.position.copy(p);an.add(rg);}
  hd.add(an);});
B.add(hd);

const th=new T.Group();th.position.set(0,0,.2);
const bdm=new T.Mesh(new T.CylinderGeometry(.12,.14,.4,8),DK);bdm.rotation.x=Math.PI/2;th.add(bdm);
for(let i=0;i<4;i++){const a=i/4*Math.PI*2,pl=new T.Mesh(new T.BoxGeometry(.08,.02,.15),BR);pl.position.set(Math.cos(a)*.14,Math.sin(a)*.14,0);pl.lookAt(pl.position.clone().multiplyScalar(5));th.add(pl);}
[-1,1].forEach(function(s){const g=mkG(.06,.015,10,s*1.5);g.position.set(s*.16,.05,0);g.rotation.y=Math.PI/2;th.add(g);});
const tg1=mkG(.05,.012,8,1);tg1.position.set(0,.13,.1);tg1.rotation.x=Math.PI/2;th.add(tg1);
const tg2=mkG(.035,.012,6,-1.33);tg2.position.set(.08,.13,.1);tg2.rotation.x=Math.PI/2;th.add(tg2);
for(let i=0;i<8;i++){const a=i/8*Math.PI*2;th.add(bolt(new T.Vector3(Math.cos(a)*.13,Math.sin(a)*.13,.12),.012));}
[-1,1].forEach(function(s){const px=new T.Group();
  px.add(new T.Mesh(new T.CylinderGeometry(.015,.015,.04,8),ST));
  const rd=new T.Mesh(new T.CylinderGeometry(.005,.005,.06,6),CU);rd.position.y=.05;px.add(rd);
  const cap=new T.Mesh(new T.CylinderGeometry(.01,.01,.005,8),BR);cap.position.y=.08;px.add(cap);
  px.position.set(s*.1,-.1,.1);th.add(px);
  const cb=new T.CubicBezierCurve3(new T.Vector3(s*.1,.1,.18),new T.Vector3(s*.15,.15,.05),new T.Vector3(s*.15,.1,-.1),new T.Vector3(s*.1,.05,-.25));
  th.add(new T.Mesh(new T.TubeGeometry(cb,20,.006,6),CU));});
for(let p=0;p<3;p++)[-1,1].forEach(function(s){const lg=new T.Group();
  const u=new T.Mesh(new T.CylinderGeometry(.01,.008,.2,6),ST);u.position.set(s*.05,-.05,.05);u.rotation.z=s*.5;u.rotation.x=-.3;lg.add(u);
  const kn=new T.Mesh(new T.SphereGeometry(.012,8,8),BR);kn.position.set(s*.12,-.12,0);lg.add(kn);
  const lo=new T.Mesh(new T.CylinderGeometry(.008,.005,.15,6),ST);lo.position.set(s*.15,-.18,0);lo.rotation.z=s*.3;lg.add(lo);
  const ft=new T.Mesh(new T.ConeGeometry(.01,.03,4),BR);ft.position.set(s*.18,-.25,0);ft.rotation.x=Math.PI;lg.add(ft);
  lg.position.set(0,0,.3-p*.15);th.add(lg);});
B.add(th);

const ab=new T.Group();ab.position.set(0,-.02,-.15);
for(let i=0;i<5;i++){const r1=.1-i*.015,r2=.11-i*.015;
  const sg=new T.Mesh(new T.CylinderGeometry(r1,r2,.1,8),i%2?CU:BR);sg.rotation.x=Math.PI/2;sg.position.z=-i*.1;ab.add(sg);
  if(i<4){const rg=new T.Mesh(new T.TorusGeometry(r2+.005,.008,8,16),ST);rg.position.z=-i*.1-.05;ab.add(rg);}}
const tcap=new T.Mesh(new T.SphereGeometry(.04,12,12),GO);tcap.position.z=-.55;ab.add(tcap);
[-1,1].forEach(function(s){for(let i=0;i<3;i++){const v=new T.Mesh(new T.CylinderGeometry(.01,.01,.03,6),DK);v.position.set(s*.08,.05,-i*.15-.05);v.rotation.z=s*.5;ab.add(v);}});
const ag=mkG(.03,.008,6,-1);ag.position.set(0,.08,-.2);ag.rotation.x=Math.PI/2;ab.add(ag);
B.add(ab);

function mkW(fore){const w=new T.Group(),sh=new T.Shape();
  if(fore){sh.moveTo(0,0);sh.bezierCurveTo(.3,.2,1.2,1,2.2,2);sh.bezierCurveTo(2.5,1.2,2,.2,1.3,-.1);sh.bezierCurveTo(.8,-.2,.3,-.1,0,0);
    hC(sh,.4,.3,.07);hC(sh,.8,.6,.06);hC(sh,1.2,.9,.05);hC(sh,1.5,1.1,.05);hC(sh,1.3,.4,.05);hC(sh,.6,.5,.04);hC(sh,1.7,.7,.04);
    hR(sh,.2,.15,.1,.07);hR(sh,.6,.4,.1,.07);hR(sh,1,.7,.08,.06);hR(sh,1.3,1,.08,.06);
  }else{sh.moveTo(0,0);sh.bezierCurveTo(.2,-.3,.8,-.8,1.4,-1.2);sh.bezierCurveTo(1.6,-.8,1.4,-.2,1,.1);sh.bezierCurveTo(.6,.2,.2,.1,0,0);
    hC(sh,.3,-.2,.06);hC(sh,.7,-.5,.05);hC(sh,.2,-.1,.04);hR(sh,.4,-.35,.08,.06);hR(sh,.8,-.6,.07,.05);}
  w.add(new T.Mesh(new T.ExtrudeGeometry(sh,{steps:1,depth:.02,bevelEnabled:true,bevelThickness:.003,bevelSize:.003,bevelSegments:1}),BR));
  (fore?[{x:.7,y:.6,a:Math.PI/4,l:2},{x:1,y:.5,a:Math.PI/3,l:1.5}]:[{x:.5,y:-.3,a:Math.PI/5,l:1.2}]).forEach(function(d){
    const rb=new T.Mesh(new T.CylinderGeometry(.008,.006,d.l,6),CU);rb.rotation.z=d.a;rb.position.set(d.x,d.y,.03);w.add(rb);});
  const gp=new T.Mesh(new T.CircleGeometry(fore?.12:.09,16),GL);gp.position.set(fore?.7:.4,fore?.6:-.3,.02);w.add(gp);
  const wgr=mkG(fore?.04:.03,.01,8,fore?1.2:-.8);wgr.position.set(fore?.7:.4,fore?.6:-.3,.03);wgr.rotation.x=Math.PI/2;w.add(wgr);
  const nr=fore?10:6;for(let i=0;i<nr;i++){const tv=i/nr,rv=new T.Mesh(new T.SphereGeometry(.008,6,6),ST);rv.position.set(fore?tv*2:tv*1.2,fore?tv*1.6:-tv*.8,.03);w.add(rv);}
  if(!fore)for(let i=0;i<3;i++){const tp=new T.Mesh(new T.ConeGeometry(.012,.06,4),BR);tp.position.set(1.1-i*.08,-1-i*.03,0);tp.rotation.x=Math.PI;tp.rotation.z=.3;w.add(tp);}
  return w;}

const WL=new T.Group(),WR=new T.Group();
const fwL=mkW(1);fwL.position.set(-.1,0,0);fwL.rotation.y=Math.PI;WL.add(fwL);
const hwL=mkW(0);hwL.position.set(-.1,-.05,-.1);hwL.rotation.y=Math.PI;WL.add(hwL);
const fwR=mkW(1);fwR.position.set(.1,0,0);WR.add(fwR);
const hwR=mkW(0);hwR.position.set(.1,-.05,-.1);WR.add(hwR);
WL.position.set(0,.1,.15);WR.position.set(0,.1,.15);B.add(WL,WR);

const pCount=200,pGeo=new T.BufferGeometry(),pArr=new Float32Array(pCount*3);
for(let i=0;i<pCount;i++){pArr[i*3]=(Math.random()-.5)*8;pArr[i*3+1]=(Math.random()-.5)*8;pArr[i*3+2]=(Math.random()-.5)*8;}
pGeo.setAttribute('position',new T.BufferAttribute(pArr,3));
S.add(new T.Points(pGeo,new T.PointsMaterial({color:0xffaa44,size:.02,transparent:true,opacity:.5,blending:T.AdditiveBlending})));

const cm=new EffectComposer(R);cm.addPass(new RenderPass(S,C));
cm.addPass(new UnrealBloomPass(new T.Vector2(innerWidth,innerHeight),.8,.4,.85));
cm.addPass(new OutputPass());

const clk=new T.Clock();
(function loop(){requestAnimationFrame(loop);const t=clk.getElapsedTime();
  const fp=Math.sin(t*2)*.3;WL.rotation.y=fp;WR.rotation.y=-fp;
  WL.rotation.z=.2+Math.sin(t*2)*.1;WR.rotation.z=-.2-Math.sin(t*2)*.1;
  gears.forEach(function(g){g.userData.sp.rotation.y+=g.userData.speed*.01;});
  const gv=2+Math.sin(t*3);GO.emissiveIntensity=gv;GB.emissiveIntensity=gv*.7;
  const ar=pGeo.attributes.position.array;
  for(let i=0;i<pCount;i++){ar[i*3+1]+=.002;if(ar[i*3+1]>4)ar[i*3+1]=-4;}
  pGeo.attributes.position.needsUpdate=true;
  B.position.y=Math.sin(t*.8)*.1;B.rotation.z=Math.sin(t*.5)*.03;
  O.update();cm.render();})();

addEventListener('resize',function(){C.aspect=innerWidth/innerHeight;C.updateProjectionMatrix();
  R.setSize(innerWidth,innerHeight);cm.setSize(innerWidth,innerHeight);});
</script>
</body>
</html>

横向对比

维度 GLM-5.3 Qwen3.8-Max MiMo-V2.6-Pro
代码量 1084 行 2738 行 168 行
美学完成度 ★★★★★ ★★★★ ★★★
功能丰富度 ★★★★ ★★★★★ ★★★
工程规范 ★★★ ★★★★★ ★★
技术炫技度 ★★★★★(全自研管线) ★★★★ ★★
人格 偏执艺术家 卷王产品经理 极简快刀手

这场测试真正有意思的地方

三只蝴蝶,其实是三种「AI 思维方式」的显影:

GLM 在回答"什么是最精美的",Qwen 在回答"什么是最完整的",MiMo 在回答"什么是最有效的"。

同一句 Prompt,模型没有问任何反问,而是各自调用了自己训练时内化的价值观。这比跑分真实多了——跑分测的是知识,同题创作测的是审美、性格和工程哲学。

2026 年了,国产大模型的代码能力早就不需要怀疑。真正拉开差距的,是那句 Prompt 之外的进化方向:当所有人都能一句话生成作品时,决定价值的是生成物的"灵魂"。

哦对了,三只蝴蝶都是单文件 HTML,零依赖、零模型文件,双击就能飞。

**你 pick 哪一只?评论区见。


素材:三只机械蝴蝶均为 AI 一次性生成,无人工修改。

posted @ 2026-09-28 08:29  Allen3  阅读(22)  评论(0)    收藏  举报