同一句话,三只国产机械蝴蝶
我把同一句话喂给三个国产大模型,让它们各造一只「机械蝴蝶」——结果差距比我想象的大多了
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">拖 拽 旋 转 · 滚 轮 缩 放</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 一次性生成,无人工修改。

浙公网安备 33010602011771号