Files
nagi-bench/outputs/deepseek-v4-pro/claude-code-max/skeleton-watch.html
T
OHCJ099 75b18a76e4 Add skeleton-watch runs: DeepSeek-V4-Pro, Gemini 3.1/3.5, GLM-5.2, Kimi K2.7, MiMo (#23)
Seven skeleton-watch runs across existing models plus the new MiMo Code harness. Validated: validate-data + update-registry --check both pass; artifacts render clean (real Chrome), no external calls beyond standard CDNs, no duplication.
2026-06-21 11:19:25 +01:00

1598 lines
50 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D 镂空机械腕表</title>
<style>
:root {
--bg: #0a0a0f;
--panel-bg: rgba(15, 15, 25, 0.95);
--gold: #c8a45a;
--gold-dim: #8a7040;
--text: #d0d0d0;
--text-dim: #888;
--accent: #e8c878;
--lume: #b8e8c0;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
overflow: hidden;
font-family: 'Crimson Text', 'Georgia', 'Segoe UI', serif;
-webkit-font-smoothing: antialiased;
user-select: none;
-webkit-user-select: none;
cursor: grab;
}
body:active { cursor: grabbing; }
body.pointer { cursor: pointer; }
#canvas-container {
position: fixed;
inset: 0;
z-index: 1;
}
canvas { display: block; }
/* 背景渐变 */
#bg-gradient {
position: fixed;
inset: 0;
z-index: 0;
background:
radial-gradient(ellipse at 50% 40%, rgba(30,30,60,0.6) 0%, transparent 70%),
radial-gradient(ellipse at 50% 100%, rgba(10,10,30,0.8) 0%, transparent 50%),
radial-gradient(ellipse at 20% 20%, rgba(20,20,50,0.4) 0%, transparent 50%);
pointer-events: none;
}
/* 顶部标题栏 */
#title-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 10;
padding: 24px 32px;
display: flex;
justify-content: space-between;
align-items: flex-start;
pointer-events: none;
}
#title-bar > * { pointer-events: auto; }
#watch-title {
color: var(--text);
font-size: 22px;
font-weight: 400;
letter-spacing: 4px;
text-transform: uppercase;
}
#watch-title span {
display: block;
font-size: 12px;
color: var(--gold);
letter-spacing: 6px;
margin-top: 2px;
font-weight: 300;
}
/* 控制按钮 */
#controls {
position: fixed;
bottom: 32px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
display: flex;
gap: 12px;
background: var(--panel-bg);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 40px;
padding: 6px;
backdrop-filter: blur(20px);
}
.ctrl-btn {
background: transparent;
border: none;
color: var(--text-dim);
padding: 10px 20px;
border-radius: 30px;
cursor: pointer;
font-family: inherit;
font-size: 13px;
letter-spacing: 2px;
text-transform: uppercase;
transition: all 0.3s ease;
white-space: nowrap;
}
.ctrl-btn:hover { color: var(--text); }
.ctrl-btn.active {
background: rgba(200,164,90,0.15);
color: var(--gold);
}
.ctrl-btn svg {
width: 16px;
height: 16px;
vertical-align: -3px;
margin-right: 4px;
}
/* 信息面板 */
#info-panel {
position: fixed;
right: 24px;
top: 50%;
transform: translateY(-50%);
z-index: 10;
background: var(--panel-bg);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 16px;
padding: 24px;
width: 240px;
backdrop-filter: blur(20px);
opacity: 0;
transition: opacity 0.4s ease;
pointer-events: none;
}
#info-panel.visible { opacity: 1; }
#info-panel h3 {
color: var(--gold);
font-size: 13px;
font-weight: 400;
letter-spacing: 3px;
text-transform: uppercase;
margin-bottom: 8px;
}
#info-panel .part-name {
color: var(--text);
font-size: 18px;
font-weight: 400;
margin-bottom: 6px;
}
#info-panel .part-desc {
color: var(--text-dim);
font-size: 12px;
line-height: 1.6;
font-weight: 300;
}
#info-panel .part-material {
display: inline-block;
margin-top: 10px;
padding: 3px 10px;
border: 1px solid rgba(255,255,255,0.12);
border-radius: 12px;
color: var(--text-dim);
font-size: 10px;
letter-spacing: 1px;
}
/* 提示标签 */
#tooltip {
position: fixed;
z-index: 20;
pointer-events: none;
background: rgba(0,0,0,0.85);
border: 1px solid rgba(200,164,90,0.4);
border-radius: 8px;
padding: 6px 14px;
color: var(--gold);
font-size: 12px;
letter-spacing: 2px;
opacity: 0;
transition: opacity 0.15s ease;
transform: translate(-50%, -130%);
white-space: nowrap;
}
#tooltip.visible { opacity: 1; }
#tooltip::after {
content: '';
position: absolute;
bottom: -5px;
left: 50%;
transform: translateX(-50%) rotate(45deg);
width: 8px;
height: 8px;
background: rgba(0,0,0,0.85);
border-right: 1px solid rgba(200,164,90,0.4);
border-bottom: 1px solid rgba(200,164,90,0.4);
}
/* 响应式 */
@media (max-width: 768px) {
#info-panel { right: 8px; width: 200px; padding: 16px; }
#info-panel .part-name { font-size: 15px; }
#title-bar { padding: 16px; }
#watch-title { font-size: 17px; }
.ctrl-btn { padding: 8px 14px; font-size: 11px; }
}
</style>
</head>
<body>
<div id="bg-gradient"></div>
<div id="canvas-container"></div>
<div id="title-bar">
<div id="watch-title">
Calibre Squelette
<span>Tourbillon · Manufacture</span>
</div>
</div>
<div id="tooltip"></div>
<div id="info-panel">
<h3>组件详情</h3>
<div class="part-name"></div>
<div class="part-desc"></div>
<div class="part-material"></div>
</div>
<div id="controls">
<button class="ctrl-btn active" id="btn-normal" onclick="setView('normal')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/></svg>
正常视图
</button>
<button class="ctrl-btn" id="btn-explode" onclick="setView('explode')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="3" x2="12" y2="9"/><line x1="12" y1="15" x2="12" y2="21"/><line x1="8" y1="6" x2="11" y2="9"/><line x1="13" y1="15" x2="16" y2="18"/><line x1="16" y1="6" x2="13" y2="9"/><line x1="11" y1="15" x2="8" y2="18"/></svg>
爆炸视图
</button>
<button class="ctrl-btn" id="btn-auto" onclick="toggleAutoRotate()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 11-6.219-8.56"/><polyline points="21 3 21 9 15 9"/></svg>
自动旋转
</button>
<button class="ctrl-btn" id="btn-reset" onclick="resetCamera()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 12"/></svg>
重置视角
</button>
</div>
<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>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// ==================== 场景初始化 ====================
const container = document.getElementById('canvas-container');
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.3;
renderer.outputColorSpace = THREE.SRGBColorSpace;
container.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x080810);
scene.fog = new THREE.Fog(0x080810, 30, 120);
const camera = new THREE.PerspectiveCamera(42, window.innerWidth / window.innerHeight, 0.5, 200);
camera.position.set(18, 14, 28);
camera.lookAt(0, 0, 0);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, -0.5, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.08;
controls.minDistance = 12;
controls.maxDistance = 65;
controls.maxPolarAngle = Math.PI * 0.7;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.4;
controls.update();
// ==================== 灯光 ====================
// 环境光
scene.add(new THREE.AmbientLight(0x1a1a30, 0.6));
// 主光源(模拟摄影棚主灯)
const keyLight = new THREE.DirectionalLight(0xffeedd, 12);
keyLight.position.set(15, 20, 10);
keyLight.castShadow = true;
keyLight.shadow.mapSize.width = 2048;
keyLight.shadow.mapSize.height = 2048;
keyLight.shadow.camera.near = 0.5;
keyLight.shadow.camera.far = 100;
keyLight.shadow.camera.left = -30;
keyLight.shadow.camera.right = 30;
keyLight.shadow.camera.top = 30;
keyLight.shadow.camera.bottom = -30;
keyLight.shadow.bias = -0.0001;
keyLight.shadow.normalBias = 0.02;
scene.add(keyLight);
// 补光(冷色调)
const fillLight = new THREE.DirectionalLight(0xaaccff, 4);
fillLight.position.set(-10, 5, -5);
scene.add(fillLight);
// 轮廓光(背光)
const rimLight = new THREE.DirectionalLight(0xffffff, 8);
rimLight.position.set(0, -2, -15);
scene.add(rimLight);
// 顶部高光
const topLight = new THREE.DirectionalLight(0xffffff, 6);
topLight.position.set(0, 25, 0);
scene.add(topLight);
// 装饰点光源(模拟珠宝射灯)
const pointLights = [];
for (let i = 0; i < 4; i++) {
const angle = (i / 4) * Math.PI * 2;
const pl = new THREE.PointLight(0xffeedd, 30, 30, 1.5);
pl.position.set(Math.cos(angle) * 18, 3, Math.sin(angle) * 18);
scene.add(pl);
pointLights.push(pl);
}
// ==================== 环境贴图(反射用) ====================
function createEnvMap() {
const pmrem = new THREE.PMREMGenerator(renderer);
const envScene = new THREE.Scene();
// 渐变天球
const skyGeo = new THREE.SphereGeometry(60, 32, 32);
const skyMat = new THREE.ShaderMaterial({
side: THREE.BackSide,
uniforms: {
topColor: { value: new THREE.Color(0x334466) },
midColor: { value: new THREE.Color(0x1a1a30) },
botColor: { value: new THREE.Color(0x0a0a18) },
},
vertexShader: `
varying vec3 vWPos;
void main() {
vec4 wp = modelMatrix * vec4(position, 1.0);
vWPos = wp.xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform vec3 topColor, midColor, botColor;
varying vec3 vWPos;
void main() {
float h = normalize(vWPos).y;
vec3 col = h > 0.0 ? mix(midColor, topColor, h) : mix(midColor, botColor, -h);
gl_FragColor = vec4(col, 1.0);
}
`,
});
envScene.add(new THREE.Mesh(skyGeo, skyMat));
// 环境中的亮点(产生反射高光)
for (let i = 0; i < 8; i++) {
const a = (i / 8) * Math.PI * 2;
const h = (Math.random() - 0.5) * 20;
const r = 25 + Math.random() * 20;
const spot = new THREE.Mesh(
new THREE.PlaneGeometry(4 + Math.random() * 6, 4 + Math.random() * 6),
new THREE.MeshBasicMaterial({
color: new THREE.Color().setHSL(0.08 + Math.random() * 0.12, 0.2, 0.3 + Math.random() * 0.7),
side: THREE.DoubleSide,
})
);
spot.position.set(Math.cos(a) * r, h, Math.sin(a) * r);
spot.lookAt(0, 0, 0);
envScene.add(spot);
}
const rt = pmrem.fromScene(envScene, 0.04);
rt.texture.colorSpace = THREE.SRGBColorSpace;
envScene.clear();
pmrem.dispose();
return rt.texture;
}
const envMap = createEnvMap();
scene.environment = envMap;
scene.backgroundIntensity = 0.1;
// ==================== 材质 ====================
const matSteel = new THREE.MeshStandardMaterial({
color: 0xd0d0d8, metalness: 0.92, roughness: 0.25, envMapIntensity: 1.2,
});
const matSteelDark = new THREE.MeshStandardMaterial({
color: 0x909098, metalness: 0.9, roughness: 0.35, envMapIntensity: 1.0,
});
const matSteelPolished = new THREE.MeshStandardMaterial({
color: 0xe8e8f0, metalness: 0.95, roughness: 0.12, envMapIntensity: 1.5,
});
const matBrass = new THREE.MeshStandardMaterial({
color: 0xd4a854, metalness: 0.94, roughness: 0.18, envMapIntensity: 1.3,
});
const matBrassLight = new THREE.MeshStandardMaterial({
color: 0xe8c878, metalness: 0.93, roughness: 0.15, envMapIntensity: 1.4,
});
const matRuby = new THREE.MeshPhysicalMaterial({
color: 0xcc1144, metalness: 0.05, roughness: 0.08, clearcoat: 0.5,
clearcoatRoughness: 0.1, emissive: 0x330011, emissiveIntensity: 0.4,
envMapIntensity: 1.8,
});
const matGold = new THREE.MeshStandardMaterial({
color: 0xf0c860, metalness: 0.98, roughness: 0.08, envMapIntensity: 1.6,
});
const matCrystal = new THREE.MeshPhysicalMaterial({
color: 0xddeeff, metalness: 0.0, roughness: 0.05, transparent: true,
opacity: 0.28, envMapIntensity: 1.2, clearcoat: 1.0, clearcoatRoughness: 0.05,
specularIntensity: 1.5, specularColor: new THREE.Color(0xffffff),
});
const matLume = new THREE.MeshStandardMaterial({
color: 0xc8f0d0, metalness: 0.05, roughness: 0.3, emissive: 0x88cc90,
emissiveIntensity: 1.8,
});
const matBlackPolish = new THREE.MeshStandardMaterial({
color: 0x1a1a22, metalness: 0.7, roughness: 0.2, envMapIntensity: 0.8,
});
// ==================== 纹理生成 ====================
function createPerlageTexture() {
const s = 512, c = document.createElement('canvas');
c.width = c.height = s;
const ctx = c.getContext('2d');
ctx.fillStyle = '#a0a0a8';
ctx.fillRect(0, 0, s, s);
const sp = 30, r = 13;
for (let row = 0; row < s / sp + 2; row++) {
const ox = (row % 2) * sp / 2;
for (let col = 0; col < s / sp + 2; col++) {
const cx = col * sp + ox, cy = row * sp;
const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, r);
g.addColorStop(0, '#c4c4cc');
g.addColorStop(0.5, '#b0b0b8');
g.addColorStop(0.85, '#9a9aa2');
g.addColorStop(1, '#888890');
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.fill();
}
}
const tex = new THREE.CanvasTexture(c);
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(1.5, 1.5);
tex.colorSpace = THREE.SRGBColorSpace;
return tex;
}
function createGenevaTexture() {
const s = 512, c = document.createElement('canvas');
c.width = c.height = s;
const ctx = c.getContext('2d');
ctx.fillStyle = '#9a9aa2';
ctx.fillRect(0, 0, s, s);
const sw = s / 24;
for (let i = 0; i < 24; i++) {
const y = i * sw;
const g = ctx.createLinearGradient(0, y, 0, y + sw);
g.addColorStop(0, '#a0a0a8');
g.addColorStop(0.25, '#c0c0c8');
g.addColorStop(0.5, '#d0d0d8');
g.addColorStop(0.75, '#c0c0c8');
g.addColorStop(1, '#a0a0a8');
ctx.fillStyle = g;
ctx.fillRect(0, y, s, sw);
}
const tex = new THREE.CanvasTexture(c);
tex.colorSpace = THREE.SRGBColorSpace;
return tex;
}
const perlageTex = createPerlageTexture();
const genevaTex = createGenevaTexture();
// 带纹理的钢板材质
const matMainPlate = matSteelDark.clone();
matMainPlate.map = perlageTex;
matMainPlate.roughness = 0.45;
const matBridge = matSteel.clone();
matBridge.map = genevaTex;
matBridge.roughness = 0.22;
// ==================== 辅助函数 ====================
function createGearShape(outerR, innerR, teeth, hubR = 0) {
const shape = new THREE.Shape();
const aPerTooth = (Math.PI * 2) / teeth;
for (let i = 0; i < teeth; i++) {
const a1 = i * aPerTooth;
const a2 = a1 + aPerTooth * 0.22;
const a3 = a1 + aPerTooth * 0.35;
const a4 = a1 + aPerTooth * 0.65;
const a5 = a1 + aPerTooth * 0.78;
if (i === 0) shape.moveTo(Math.cos(a1) * innerR, Math.sin(a1) * innerR);
else shape.lineTo(Math.cos(a1) * innerR, Math.sin(a1) * innerR);
shape.lineTo(Math.cos(a2) * innerR, Math.sin(a2) * innerR);
shape.lineTo(Math.cos(a3) * outerR, Math.sin(a3) * outerR);
shape.lineTo(Math.cos(a4) * outerR, Math.sin(a4) * outerR);
shape.lineTo(Math.cos(a5) * innerR, Math.sin(a5) * innerR);
}
shape.closePath();
// 中心孔
const h = new THREE.Path();
h.absarc(0, 0, hubR || innerR * 0.22, 0, Math.PI * 2, true);
shape.holes.push(h);
// 辐条镂空(仅大齿轮)
if (innerR > 3 && teeth > 15) {
const spokeR = (innerR + (hubR || innerR * 0.22)) / 2;
const cutR = (innerR - (hubR || innerR * 0.22)) * 0.32;
const nSpokes = 4;
for (let i = 0; i < nSpokes; i++) {
const a = (i / nSpokes) * Math.PI * 2 + Math.PI / nSpokes;
const sh = new THREE.Path();
sh.absarc(Math.cos(a) * spokeR, Math.sin(a) * spokeR, cutR, 0, Math.PI * 2, true);
shape.holes.push(sh);
}
}
return shape;
}
function createGearMesh(outerR, innerR, teeth, thickness, material, hubR) {
const shape = createGearShape(outerR, innerR, teeth, hubR);
const geom = new THREE.ExtrudeGeometry(shape, {
depth: thickness, bevelEnabled: true,
bevelThickness: thickness * 0.08, bevelSize: thickness * 0.04, bevelSegments: 3,
});
const mesh = new THREE.Mesh(geom, material);
mesh.position.y = -thickness / 2;
mesh.castShadow = true;
mesh.receiveShadow = true;
return mesh;
}
function createRing(outerR, innerR, depth, material) {
const shape = new THREE.Shape();
shape.absarc(0, 0, outerR, 0, Math.PI * 2, false);
const hole = new THREE.Path();
hole.absarc(0, 0, innerR, 0, Math.PI * 2, true);
shape.holes.push(hole);
const geom = new THREE.ExtrudeGeometry(shape, {
depth, bevelEnabled: true, bevelThickness: 0.15, bevelSize: 0.1, bevelSegments: 3,
});
const mesh = new THREE.Mesh(geom, material);
mesh.position.y = -depth / 2;
mesh.castShadow = true;
mesh.receiveShadow = true;
return mesh;
}
// ==================== 创建表壳 ====================
const watchGroup = new THREE.Group();
scene.add(watchGroup);
// --- 表壳中框 ---
const caseMiddle = createRing(21, 17.5, 3.2, matSteelPolished);
caseMiddle.name = '表壳中框';
caseMiddle.userData = {
name: '表壳中框',
desc: '904L精钢表壳中框,经过精细抛光和拉丝处理。保护内部机芯并提供结构支撑。',
material: '904L 精钢',
};
watchGroup.add(caseMiddle);
// --- 表圈 ---
const bezelShape = new THREE.Shape();
// 使用LatheGeometry创建表圈轮廓
const bezelPoints = [
new THREE.Vector2(18.2, 0), new THREE.Vector2(18.8, 0.2),
new THREE.Vector2(19.3, 0.6), new THREE.Vector2(19.5, 1.0),
new THREE.Vector2(20.8, 1.3), new THREE.Vector2(20.6, 1.8),
new THREE.Vector2(19.5, 2.0), new THREE.Vector2(18.5, 2.0),
new THREE.Vector2(18.5, 2.5), new THREE.Vector2(20.8, 2.5),
new THREE.Vector2(20.8, 0),
];
const bezelGeom = new THREE.LatheGeometry(bezelPoints, 96);
const bezel = new THREE.Mesh(bezelGeom, matSteelPolished);
bezel.position.y = 1.6;
bezel.name = '表圈';
bezel.userData = {
name: '表圈',
desc: '抛光精钢表圈,固定蓝宝石水晶玻璃表镜。斜面设计增添优雅线条。',
material: '904L 精钢 · 抛光',
};
bezel.castShadow = true;
bezel.receiveShadow = true;
watchGroup.add(bezel);
// --- 表底盖 ---
const caseBackOuter = createRing(20.8, 16.5, 0.6, matSteelPolished);
caseBackOuter.position.y = -3.3;
caseBackOuter.name = '表底盖';
caseBackOuter.userData = {
name: '表底盖',
desc: '蓝宝石水晶透底表背,展示精美机芯运转。',
material: '904L 精钢 · 蓝宝石水晶',
};
watchGroup.add(caseBackOuter);
// 透底水晶
const displayCrystal = new THREE.Mesh(
new THREE.CylinderGeometry(16.2, 16.2, 0.3, 64),
matCrystal.clone(),
);
displayCrystal.position.y = -3.3;
displayCrystal.userData = {
name: '透底水晶',
desc: '蓝宝石水晶玻璃底盖,具有防刮擦性能,展现机芯全貌。',
material: '蓝宝石水晶玻璃',
};
watchGroup.add(displayCrystal);
// --- 表耳 ---
function createLug() {
const lugGroup = new THREE.Group();
// 使用RoundedBox替代方案
const shape = new THREE.Shape();
const w = 3.0, l = 5.5;
shape.moveTo(-w / 2, 0);
shape.quadraticCurveTo(-w / 2 + 0.8, l, 0, l + 0.3);
shape.quadraticCurveTo(w / 2 - 0.8, l, w / 2, 0);
shape.closePath();
const geom = new THREE.ExtrudeGeometry(shape, {
depth: 1.8, bevelEnabled: true, bevelThickness: 0.4, bevelSize: 0.3, bevelSegments: 5,
});
const mesh = new THREE.Mesh(geom, matSteelPolished);
mesh.position.set(0, -1.8 / 2, 0);
mesh.castShadow = true;
mesh.receiveShadow = true;
lugGroup.add(mesh);
// 弹簧杆孔
const pinHole = new THREE.Mesh(
new THREE.CylinderGeometry(0.35, 0.35, 1.4, 16),
matSteelDark,
);
pinHole.rotation.x = Math.PI / 2;
pinHole.position.set(0, 0.2, l + 1.5);
pinHole.userData = { name: '弹簧杆', desc: '用于连接表带的弹簧杆', material: '精钢' };
lugGroup.add(pinHole);
return lugGroup;
}
const lugPositions = [
{ x: 7.5, z: 21.5, ry: 0 }, { x: -7.5, z: 21.5, ry: 0 },
{ x: 7.5, z: -21.5, ry: Math.PI }, { x: -7.5, z: -21.5, ry: Math.PI },
];
lugPositions.forEach(p => {
const lug = createLug();
lug.position.set(p.x, -2.6, p.z);
lug.rotation.y = p.ry;
lug.name = '表耳';
lug.userData = { name: '表耳', desc: '一体式表耳,优雅过渡至表带', material: '904L 精钢' };
watchGroup.add(lug);
});
// --- 表冠 ---
function createCrown() {
const crownGrp = new THREE.Group();
// 表冠主体
const body = new THREE.Mesh(
new THREE.CylinderGeometry(2.2, 2.4, 3.5, 40),
matSteelPolished,
);
body.castShadow = true;
crownGrp.add(body);
// 凹槽环
for (let i = 0; i < 7; i++) {
const y = -1.75 + (i + 0.5) * 3.5 / 7;
const groove = new THREE.Mesh(
new THREE.TorusGeometry(2.25, 0.15, 8, 40),
matSteelDark,
);
groove.position.y = y;
groove.rotation.x = Math.PI / 2;
crownGrp.add(groove);
}
// 表冠顶盖
const cap = new THREE.Mesh(
new THREE.CylinderGeometry(2.2, 2.2, 0.3, 40),
matSteelPolished,
);
cap.position.y = 1.9;
cap.castShadow = true;
crownGrp.add(cap);
// 顶部logo凹槽
const logoRing = new THREE.Mesh(
new THREE.TorusGeometry(0.9, 0.15, 8, 32),
matSteel,
);
logoRing.position.y = 2.1;
logoRing.rotation.x = Math.PI / 2;
crownGrp.add(logoRing);
return crownGrp;
}
const crownGroup = new THREE.Group();
crownGroup.name = '表冠';
crownGroup.userData = {
name: '表冠',
desc: '旋入式表冠,带凹槽防滑设计。用于上链和调校时间。',
material: '904L 精钢 · 抛光',
};
const crownMesh = createCrown();
crownGroup.add(crownMesh);
crownGroup.position.set(22, -1.6, 0);
crownGroup.rotation.z = -Math.PI / 2;
watchGroup.add(crownGroup);
// 表冠轴
const stemGeom = new THREE.CylinderGeometry(0.7, 0.9, 4, 20);
const stem = new THREE.Mesh(stemGeom, matSteel);
stem.rotation.z = Math.PI / 2;
stem.position.set(18.5, -1.6, 0);
stem.name = '表冠轴';
stem.userData = { name: '表冠轴', desc: '连接表冠与机芯的上链柄轴', material: '精钢' };
watchGroup.add(stem);
// ==================== 蓝宝石表镜 ====================
const crystalGeom = new THREE.SphereGeometry(18.3, 80, 40, 0, Math.PI * 2, 0, Math.PI * 0.155);
const crystal = new THREE.Mesh(crystalGeom, matCrystal);
crystal.position.y = 2.15;
crystal.name = '蓝宝石表镜';
crystal.userData = {
name: '蓝宝石水晶表镜',
desc: '双面防反光处理蓝宝石水晶玻璃,莫氏硬度9,仅次于钻石。',
material: '蓝宝石水晶 · 防反光涂层',
};
watchGroup.add(crystal);
// ==================== 机芯组件 ====================
const movementRoot = new THREE.Group();
movementRoot.name = 'movementRoot';
watchGroup.add(movementRoot);
// --- 主夹板 ---
const mainPlate = new THREE.Mesh(
new THREE.CylinderGeometry(16.8, 16.8, 1.0, 80),
matMainPlate,
);
mainPlate.position.y = -1.6;
mainPlate.name = '主夹板';
mainPlate.userData = {
name: '主夹板',
desc: '机芯基础结构,承载所有齿轮系和功能模块。饰以精美鱼鳞纹(Perlage)打磨。',
material: '黄铜镀铑 · 鱼鳞纹打磨',
};
mainPlate.castShadow = true;
mainPlate.receiveShadow = true;
movementRoot.add(mainPlate);
// 主夹板上的珠宝轴承
for (let i = 0; i < 8; i++) {
const a = (i / 8) * Math.PI * 2 + Math.PI / 8;
const dist = 5 + (i % 3) * 4;
const jewel = new THREE.Mesh(new THREE.SphereGeometry(0.5, 20, 20), matRuby);
jewel.position.set(Math.cos(a) * dist, -1.05, Math.sin(a) * dist);
jewel.name = '红宝石轴承';
jewel.userData = {
name: '红宝石轴承',
desc: '合成红宝石轴承(Jewel),减少齿轮枢轴的摩擦和磨损。',
material: '合成红宝石',
};
movementRoot.add(jewel);
// 黄金套筒
const chaton = new THREE.Mesh(
new THREE.TorusGeometry(0.65, 0.12, 8, 20),
matGold,
);
chaton.position.copy(jewel.position);
chaton.rotation.x = Math.PI / 2;
chaton.name = '黄金套筒';
chaton.userData = { name: '黄金套筒', desc: 'K金套筒(Chaton),固定红宝石轴承', material: '18K 黄金' };
movementRoot.add(chaton);
}
// --- 齿轮系 ---
const gearY = -1.05;
const gearGroup = new THREE.Group();
gearGroup.name = 'gearGroup';
movementRoot.add(gearGroup);
// 发条盒
const barrelOuter = new THREE.Mesh(
new THREE.CylinderGeometry(4.5, 4.5, 1.8, 60),
matBrass,
);
barrelOuter.position.set(8, gearY, 3);
barrelOuter.name = '发条盒';
barrelOuter.userData = {
name: '发条盒',
desc: '容纳主发条的动力储存装置。提供约72小时动力储备。',
material: '黄铜',
};
barrelOuter.castShadow = true;
gearGroup.add(barrelOuter);
// 发条盒盖(棘轮)
const ratchetWheel = createGearMesh(4.2, 3.8, 30, 0.35, matBrassLight, 1.0);
ratchetWheel.position.set(8, gearY + 0.9, 3);
ratchetWheel.name = '棘轮';
ratchetWheel.userData = {
name: '大钢轮(棘轮)',
desc: '上链时与棘爪配合,防止发条反转。',
material: '黄铜',
};
gearGroup.add(ratchetWheel);
// 中心轮
const centerWheel = createGearMesh(5.5, 5.0, 48, 0.4, matBrass, 1.2);
centerWheel.position.set(0, gearY, 0);
centerWheel.name = '中心轮';
centerWheel.userData = {
name: '中心轮(二轮)',
desc: '齿轮系中的第二齿轮,驱动分针。与发条盒齿轮啮合。',
material: '黄铜',
};
gearGroup.add(centerWheel);
// 三轮
const thirdWheel = createGearMesh(4.0, 3.5, 36, 0.3, matBrass, 0.9);
thirdWheel.position.set(-4, gearY, -4);
thirdWheel.name = '三轮';
thirdWheel.userData = {
name: '三轮(过轮)',
desc: '将动力从中心轮传递至四轮,实现齿轮减速。',
material: '黄铜',
};
gearGroup.add(thirdWheel);
// 四轮(秒轮)
const fourthWheel = createGearMesh(3.2, 2.8, 28, 0.25, matBrass, 0.7);
fourthWheel.position.set(5, gearY, -7);
fourthWheel.name = '四轮';
fourthWheel.userData = {
name: '四轮(秒轮)',
desc: '驱动秒针,每秒前进一格。与擒纵轮直接啮合。',
material: '黄铜',
};
gearGroup.add(fourthWheel);
// --- 擒纵机构 ---
const escapementGroup = new THREE.Group();
escapementGroup.name = 'escapementGroup';
movementRoot.add(escapementGroup);
// 擒纵轮(特殊齿形)
function createEscapeWheelShape(radius, teeth) {
const shape = new THREE.Shape();
for (let i = 0; i < teeth; i++) {
const a = (i / teeth) * Math.PI * 2;
const na = ((i + 1) / teeth) * Math.PI * 2;
const ma = (a + na) / 2;
const tipR = radius * 1.3;
const valleyR = radius * 0.65;
if (i === 0) shape.moveTo(Math.cos(a) * valleyR, Math.sin(a) * valleyR);
else shape.lineTo(Math.cos(a) * valleyR, Math.sin(a) * valleyR);
shape.lineTo(Math.cos(ma) * tipR, Math.sin(ma) * tipR);
}
shape.closePath();
const h = new THREE.Path();
h.absarc(0, 0, radius * 0.2, 0, Math.PI * 2, true);
shape.holes.push(h);
return shape;
}
const escShape = createEscapeWheelShape(2.5, 15);
const escGeom = new THREE.ExtrudeGeometry(escShape, {
depth: 0.2, bevelEnabled: true, bevelThickness: 0.03, bevelSegments: 3,
});
const escapeWheel = new THREE.Mesh(escGeom, matSteelPolished);
escapeWheel.position.set(-2, gearY + 0.15, -9);
escapeWheel.name = '擒纵轮';
escapeWheel.userData = {
name: '擒纵轮(Escapement Wheel)',
desc: '特殊齿形的擒纵轮,与擒纵叉配合将连续旋转转化为间歇运动。',
material: '精钢 · 镜面抛光',
};
escapeWheel.castShadow = true;
escapementGroup.add(escapeWheel);
// 擒纵叉
function createPalletFork() {
const grp = new THREE.Group();
// 叉体
const forkShape = new THREE.Shape();
const fl = 4.5, fw = 0.45;
forkShape.moveTo(-fl, -fw);
forkShape.lineTo(fl, -fw);
forkShape.lineTo(fl + 0.6, -fw - 0.5);
forkShape.lineTo(fl + 0.6, -fw - 1.2);
forkShape.lineTo(fl + 0.2, -fw - 1.5);
forkShape.lineTo(fl - 0.2, -fw - 1.5);
forkShape.lineTo(fl - 0.6, -fw - 1.2);
forkShape.lineTo(fl - 0.6, -fw - 0.5);
forkShape.lineTo(fl, -fw);
forkShape.lineTo(fl, fw);
forkShape.lineTo(fl + 0.6, fw + 0.5);
forkShape.lineTo(fl + 0.6, fw + 1.2);
forkShape.lineTo(fl + 0.2, fw + 1.5);
forkShape.lineTo(fl - 0.2, fw + 1.5);
forkShape.lineTo(fl - 0.6, fw + 1.2);
forkShape.lineTo(fl - 0.6, fw + 0.5);
forkShape.lineTo(fl, fw);
forkShape.closePath();
const hole = new THREE.Path();
hole.absarc(0, 0, 0.35, 0, Math.PI * 2, true);
forkShape.holes.push(hole);
const geom = new THREE.ExtrudeGeometry(forkShape, {
depth: 0.2, bevelEnabled: true, bevelThickness: 0.03, bevelSegments: 3,
});
const mesh = new THREE.Mesh(geom, matSteelPolished);
mesh.position.y = -0.1;
mesh.castShadow = true;
grp.add(mesh);
// 叉瓦
for (let s = -1; s <= 1; s += 2) {
const jewel = new THREE.Mesh(
new THREE.BoxGeometry(0.4, 0.15, 0.8),
matRuby,
);
jewel.position.set(s * 4.2, 0, 0);
grp.add(jewel);
}
return grp;
}
const palletFork = createPalletFork();
palletFork.position.set(3.5, gearY + 0.1, -9);
palletFork.name = '擒纵叉';
palletFork.userData = {
name: '擒纵叉(Pallet Fork)',
desc: '擒纵机构核心组件。红宝石叉瓦交替锁定和释放擒纵轮,将能量传递给摆轮。',
material: '精钢 · 合成红宝石叉瓦',
};
escapementGroup.add(palletFork);
// --- 摆轮组件 ---
const balanceGroup = new THREE.Group();
balanceGroup.name = 'balanceGroup';
movementRoot.add(balanceGroup);
// 摆轮外环
const balanceRim = new THREE.Mesh(
new THREE.TorusGeometry(5.5, 0.55, 16, 80),
matSteelPolished,
);
balanceRim.position.y = gearY + 0.2;
balanceRim.name = '摆轮';
balanceRim.userData = {
name: '摆轮(Balance Wheel)',
desc: '摆轮是机械表的心脏。以28,800次/小时(4Hz)的频率振荡,调节时间精度。',
material: '铍青铜 · 镜面抛光',
};
balanceRim.castShadow = true;
balanceGroup.add(balanceRim);
// 摆轮辐条
for (let i = 0; i < 4; i++) {
const a = (i / 4) * Math.PI * 2;
const spoke = new THREE.Mesh(
new THREE.BoxGeometry(0.35, 0.2, 5.0),
matSteel,
);
spoke.position.set(Math.cos(a) * 2.5, gearY + 0.2, Math.sin(a) * 2.5);
spoke.rotation.y = -a;
balanceGroup.add(spoke);
}
// 摆轮配重螺丝
for (let i = 0; i < 8; i++) {
const a = (i / 8) * Math.PI * 2;
const screw = new THREE.Mesh(
new THREE.CylinderGeometry(0.25, 0.25, 0.3, 12),
matGold,
);
screw.position.set(Math.cos(a) * 5.5, gearY + 0.35, Math.sin(a) * 5.5);
balanceGroup.add(screw);
}
// 摆轮中心轴
const balanceStaff = new THREE.Mesh(
new THREE.CylinderGeometry(0.3, 0.45, 1.0, 16),
matSteelPolished,
);
balanceStaff.position.y = gearY;
balanceGroup.add(balanceStaff);
// --- 游丝 ---
function createHairspring(turns, maxR, height, segments) {
const pts = [];
const total = turns * segments;
for (let i = 0; i <= total; i++) {
const t = i / total;
const r = 0.5 + t * (maxR - 0.5);
const a = t * turns * Math.PI * 2;
pts.push(new THREE.Vector3(Math.cos(a) * r, 0, Math.sin(a) * r));
}
const curve = new THREE.CatmullRomCurve3(pts);
return new THREE.TubeGeometry(curve, total, 0.08, 6, false);
}
const hairspringGeom = createHairspring(8, 4.8, 0.1, 200);
const hairspring = new THREE.Mesh(hairspringGeom, new THREE.MeshStandardMaterial({
color: 0x3355aa, metalness: 0.85, roughness: 0.15, envMapIntensity: 1.0,
}));
hairspring.position.y = gearY + 0.25;
hairspring.name = '游丝';
hairspring.userData = {
name: '游丝(Hairspring)',
desc: 'Nivarox合金游丝,阿基米德螺旋线形状。与摆轮组成谐振系统,决定走时精度。',
material: 'Nivarox 合金 · 蓝色氧化',
};
balanceGroup.add(hairspring);
// 摆夹板
const balanceCockShape = new THREE.Shape();
balanceCockShape.moveTo(-2, -7);
balanceCockShape.lineTo(2, -7);
balanceCockShape.lineTo(3, -4);
balanceCockShape.lineTo(2.5, 1);
balanceCockShape.lineTo(1.5, 2.5);
balanceCockShape.lineTo(-1.5, 2.5);
balanceCockShape.lineTo(-2.5, 1);
balanceCockShape.lineTo(-3, -4);
balanceCockShape.closePath();
const balanceCockHole = new THREE.Path();
balanceCockHole.absarc(0, -3.5, 1.0, 0, Math.PI * 2, true);
balanceCockShape.holes.push(balanceCockHole);
const balanceCock = new THREE.Mesh(
new THREE.ExtrudeGeometry(balanceCockShape, {
depth: 0.35, bevelEnabled: true, bevelThickness: 0.08, bevelSegments: 5,
}),
matBridge,
);
balanceCock.position.set(0, gearY + 0.8, -8);
balanceCock.name = '摆夹板';
balanceCock.userData = {
name: '摆夹板(Balance Cock)',
desc: '单边固定摆轮组件的夹板。饰以日内瓦波纹(Côtes de Genève)打磨。',
material: '黄铜镀铑 · 日内瓦波纹',
};
balanceCock.castShadow = true;
movementRoot.add(balanceCock);
// 摆夹板上的宝石
const bcJewel = new THREE.Mesh(new THREE.SphereGeometry(0.5, 20, 20), matRuby);
bcJewel.position.set(0, gearY + 1.05, -4.5);
bcJewel.name = '托石';
bcJewel.userData = { name: '托石(Cap Jewel)', desc: '摆轮上端宝石轴承,承受轴向推力', material: '合成红宝石' };
movementRoot.add(bcJewel);
// --- 夹板(桥板) ---
const bridgeY = -0.55;
// 轮系夹板
function createTrainBridge() {
const shape = new THREE.Shape();
// 覆盖中心轮和三轮区域
shape.absarc(-2, -2, 5.5, -Math.PI * 0.6, Math.PI * 0.6, false);
shape.lineTo(3, -7);
shape.absarc(0, 0, 7, -Math.PI * 0.5, Math.PI * 0.2, true);
shape.lineTo(-4, 2);
shape.closePath();
// 宝石孔
const positions = [[0, 0], [-4, -4], [5, -7]];
positions.forEach(([x, z]) => {
const h = new THREE.Path();
h.absarc(x, z, 0.7, 0, Math.PI * 2, true);
shape.holes.push(h);
});
const geom = new THREE.ExtrudeGeometry(shape, {
depth: 0.4, bevelEnabled: true, bevelThickness: 0.1, bevelSegments: 5,
});
const mesh = new THREE.Mesh(geom, matBridge);
mesh.position.y = bridgeY;
mesh.castShadow = true;
mesh.receiveShadow = true;
return mesh;
}
const trainBridge = createTrainBridge();
trainBridge.name = '轮系夹板';
trainBridge.userData = {
name: '轮系夹板(Train Bridge)',
desc: '固定中心轮、三轮、四轮的夹板。饰以日内瓦波纹,边缘倒角抛光。',
material: '黄铜镀铑 · 日内瓦波纹',
};
movementRoot.add(trainBridge);
// 夹板宝石
[[0, 0], [-4, -4], [5, -7]].forEach(([x, z]) => {
const j = new THREE.Mesh(new THREE.SphereGeometry(0.5, 20, 20), matRuby);
j.position.set(x, bridgeY + 0.25, z);
j.userData = { name: '宝石轴承', desc: '轮系上端宝石轴承', material: '合成红宝石' };
movementRoot.add(j);
const ch = new THREE.Mesh(new THREE.TorusGeometry(0.65, 0.12, 8, 20), matGold);
ch.position.set(x, bridgeY + 0.25, z);
ch.rotation.x = Math.PI / 2;
movementRoot.add(ch);
});
// 发条盒夹板
const barrelBridgeShape = new THREE.Shape();
barrelBridgeShape.absarc(8, 3, 5.5, -Math.PI * 0.2, Math.PI * 1.2, false);
barrelBridgeShape.lineTo(8, 0);
barrelBridgeShape.absarc(4, -1, 6, Math.PI * 0.5, -Math.PI * 0.3, true);
barrelBridgeShape.closePath();
const bbHole = new THREE.Path();
bbHole.absarc(8, 3, 1.0, 0, Math.PI * 2, true);
barrelBridgeShape.holes.push(bbHole);
const barrelBridge = new THREE.Mesh(
new THREE.ExtrudeGeometry(barrelBridgeShape, {
depth: 0.4, bevelEnabled: true, bevelThickness: 0.1, bevelSegments: 5,
}),
matBridge,
);
barrelBridge.position.y = bridgeY;
barrelBridge.name = '发条盒夹板';
barrelBridge.userData = {
name: '发条盒夹板(Barrel Bridge)',
desc: '固定发条盒组件的独立夹板。确保主发条稳定运转。',
material: '黄铜镀铑 · 日内瓦波纹',
};
barrelBridge.castShadow = true;
movementRoot.add(barrelBridge);
// 发条盒夹板宝石
const bbJewel = new THREE.Mesh(new THREE.SphereGeometry(0.5, 20, 20), matRuby);
bbJewel.position.set(8, bridgeY + 0.25, 3);
bbJewel.userData = { name: '宝石轴承', desc: '发条盒上端宝石轴承', material: '合成红宝石' };
movementRoot.add(bbJewel);
// ==================== 表盘与刻度 ====================
const dialGroup = new THREE.Group();
dialGroup.name = 'dialGroup';
watchGroup.add(dialGroup);
// 刻度环
const chapterRing = createRing(17.8, 15.2, 0.3, matSteelPolished);
chapterRing.position.y = 0.3;
chapterRing.name = '刻度环';
chapterRing.userData = {
name: '刻度环(Chapter Ring)',
desc: '承载时标的环形结构。镂空设计展现下方机芯。',
material: '精钢 · 抛光',
};
dialGroup.add(chapterRing);
// 时标
const markerGeom = new THREE.BoxGeometry(0.6, 0.25, 2.5);
for (let i = 0; i < 12; i++) {
const angle = (i / 12) * Math.PI * 2 + Math.PI / 2;
const r = 16.5;
const isHour = i % 3 === 0;
// 刻度主体
const marker = new THREE.Mesh(
isHour ? new THREE.BoxGeometry(0.8, 0.25, 2.8) : markerGeom,
matSteelPolished,
);
marker.position.set(Math.cos(angle) * r, 0.55, Math.sin(angle) * r);
marker.rotation.y = -(angle - Math.PI / 2);
marker.castShadow = true;
marker.name = '时标';
marker.userData = { name: '时标', desc: `${i === 0 ? 12 : i}点刻度`, material: '精钢' };
dialGroup.add(marker);
// 夜光点
const lumeDot = new THREE.Mesh(
new THREE.CylinderGeometry(0.4, 0.4, 0.15, 16),
matLume,
);
lumeDot.position.set(Math.cos(angle) * (r + 0.2), 0.68, Math.sin(angle) * (r + 0.2));
dialGroup.add(lumeDot);
}
// ==================== 指针 ====================
const handsGroup = new THREE.Group();
handsGroup.name = 'handsGroup';
watchGroup.add(handsGroup);
function createHand(length, width, taperWidth, material, lumeLength) {
const grp = new THREE.Group();
const shape = new THREE.Shape();
shape.moveTo(-width / 2, 0);
shape.lineTo(-taperWidth / 2, length);
shape.lineTo(taperWidth / 2, length);
shape.lineTo(width / 2, 0);
shape.closePath();
const geom = new THREE.ExtrudeGeometry(shape, {
depth: 0.12, bevelEnabled: true, bevelThickness: 0.03, bevelSegments: 3,
});
const mesh = new THREE.Mesh(geom, material);
mesh.position.set(0, 0, -0.06);
mesh.castShadow = true;
grp.add(mesh);
// 夜光填充
if (lumeLength > 0) {
const lumeShape = new THREE.Shape();
lumeShape.moveTo(-width * 0.35, length * 0.3);
lumeShape.lineTo(-taperWidth * 0.4, length - 1);
lumeShape.lineTo(taperWidth * 0.4, length - 1);
lumeShape.lineTo(width * 0.35, length * 0.3);
lumeShape.closePath();
const lumeGeom = new THREE.ExtrudeGeometry(lumeShape, {
depth: 0.10, bevelEnabled: true, bevelThickness: 0.02, bevelSegments: 2,
});
const lume = new THREE.Mesh(lumeGeom, matLume);
lume.position.y = 0.06;
grp.add(lume);
}
return grp;
}
// 时针
const hourHand = createHand(8.5, 1.2, 0.5, matSteelPolished, 6);
hourHand.position.y = 0.8;
hourHand.name = '时针';
hourHand.userData = { name: '时针', desc: '指示小时。镂空设计配合夜光涂层。', material: '精钢 · 夜光涂层' };
handsGroup.add(hourHand);
// 分针
const minuteHand = createHand(13, 0.9, 0.35, matSteelPolished, 10);
minuteHand.position.y = 0.85;
minuteHand.name = '分针';
minuteHand.userData = { name: '分针', desc: '指示分钟。细长优雅的设计。', material: '精钢 · 夜光涂层' };
handsGroup.add(minuteHand);
// 秒针
const secShape = new THREE.Shape();
secShape.moveTo(-0.15, -3);
secShape.lineTo(-0.1, 14);
secShape.lineTo(0.1, 14);
secShape.lineTo(0.15, -3);
secShape.closePath();
const secGeom = new THREE.ExtrudeGeometry(secShape, {
depth: 0.08, bevelEnabled: true, bevelThickness: 0.02, bevelSegments: 2,
});
const secondHand = new THREE.Mesh(secGeom, matSteelPolished);
secondHand.position.set(0, 0.9, -0.04);
secondHand.name = '秒针';
secondHand.userData = { name: '秒针', desc: '指示秒钟。细长优雅设计,带配重尾端。', material: '精钢 · 烤蓝' };
handsGroup.add(secondHand);
// 秒针夜光圆点
const secLume = new THREE.Mesh(new THREE.CylinderGeometry(0.35, 0.35, 0.1, 16), matLume);
secLume.position.set(0, 0.95, 5);
handsGroup.add(secLume);
// 指针中轴盖
const centerCap = new THREE.Mesh(
new THREE.CylinderGeometry(1.0, 1.2, 0.3, 32),
matSteelPolished,
);
centerCap.position.y = 0.95;
centerCap.name = '中轴盖';
handsGroup.add(centerCap);
// ==================== 地面反射 ====================
const groundGeom = new THREE.PlaneGeometry(80, 80);
const ground = new THREE.Mesh(groundGeom, new THREE.MeshStandardMaterial({
color: 0x111118, metalness: 0.5, roughness: 0.35, envMapIntensity: 0.7,
}));
ground.rotation.x = -Math.PI / 2;
ground.position.y = -8;
ground.receiveShadow = true;
scene.add(ground);
// ==================== 组织层级用于爆炸视图 ====================
const explodeGroups = [
{ group: crystal, name: 'crystal', normalY: 2.15, explodeY: 8.5 },
{ group: bezel, name: 'bezel', normalY: 1.6, explodeY: 5.5 },
{ group: handsGroup, name: 'hands', normalY: 0, explodeY: 4.5 },
{ group: dialGroup, name: 'dial', normalY: 0, explodeY: 2.5 },
{ group: movementRoot, name: 'movement', normalY: 0, explodeY: -0.5 },
{ group: caseMiddle, name: 'caseMiddle', normalY: 0, explodeY: -2.0 },
{ group: caseBackOuter, name: 'caseBack', normalY: -3.3, explodeY: -7.0 },
{ group: displayCrystal, name: 'displayCrystal', normalY: -3.3, explodeY: -7.0 },
{ group: crownGroup, name: 'crown', normalY: -1.6, explodeY: -1.6 },
{ group: stem, name: 'stem', normalY: -1.6, explodeY: -1.6 },
];
// 同样独立移动的子组件
const extraExplodeTargets = [];
[balanceCock, trainBridge, barrelBridge].forEach(b => {
extraExplodeTargets.push({ group: b, normalY: b.position.y, explodeY: b.position.y + 2.0 });
});
[balanceRim, ...balanceGroup.children.filter(c => c !== balanceRim && c !== hairspring && c !== balanceStaff)].forEach(b => {
// 摆轮组件已经在 movementRoot 中,不需要单独移动
});
// ==================== 交互 ====================
const raycaster = new THREE.Raycaster();
raycaster.far = 40;
const mouse = new THREE.Vector2();
let hoveredObject = null;
let selectedObject = null;
const tooltip = document.getElementById('tooltip');
const infoPanel = document.getElementById('info-panel');
let allInteractive = [];
function collectInteractive(obj) {
if (obj.userData && obj.userData.name) allInteractive.push(obj);
if (obj.children) obj.children.forEach(collectInteractive);
}
collectInteractive(watchGroup);
allInteractive.push(crystal, bezel, caseMiddle, caseBackOuter, displayCrystal);
// 高亮材质缓存
const highlightCache = new Map();
function getHighlightMaterial(obj) {
if (highlightCache.has(obj)) return highlightCache.get(obj);
const origMat = obj.material;
if (!origMat || !origMat.isMaterial) return null;
let hlMat;
if (origMat.emissive) {
hlMat = origMat.clone();
hlMat.emissive = new THREE.Color(0x886633);
hlMat.emissiveIntensity = (origMat.emissiveIntensity || 0) + 0.6;
} else {
hlMat = origMat.clone();
hlMat.emissive = new THREE.Color(0x664422);
hlMat.emissiveIntensity = 0.5;
}
highlightCache.set(obj, hlMat);
return hlMat;
}
document.addEventListener('mousemove', (e) => {
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(allInteractive, true);
let newHovered = null;
if (intersects.length > 0) {
let obj = intersects[0].object;
while (obj) {
if (obj.userData && obj.userData.name) { newHovered = obj; break; }
obj = obj.parent;
}
}
if (hoveredObject !== newHovered) {
// 还原上一个
if (hoveredObject && hoveredObject !== selectedObject) {
const origMat = highlightCache.get(hoveredObject);
if (origMat) hoveredObject.material = hoveredObject.userData?._origMat || hoveredObject.material;
}
// 高亮新的
if (newHovered) {
if (!newHovered.userData._origMat) {
newHovered.userData._origMat = newHovered.material;
}
newHovered.material = getHighlightMaterial(newHovered);
document.body.classList.add('pointer');
} else {
document.body.classList.remove('pointer');
}
hoveredObject = newHovered;
}
// 更新提示位置
if (hoveredObject && intersects.length > 0) {
const pt = intersects[0].point.clone();
pt.project(camera);
const sx = (pt.x * 0.5 + 0.5) * window.innerWidth;
const sy = (-pt.y * 0.5 + 0.5) * window.innerHeight;
tooltip.style.left = sx + 'px';
tooltip.style.top = sy + 'px';
tooltip.textContent = hoveredObject.userData.name;
tooltip.classList.add('visible');
} else {
tooltip.classList.remove('visible');
}
});
document.addEventListener('click', (e) => {
if (e.target.closest('#controls') || e.target.closest('#info-panel')) return;
if (hoveredObject && hoveredObject.userData.name) {
selectedObject = hoveredObject;
infoPanel.querySelector('.part-name').textContent = selectedObject.userData.name;
infoPanel.querySelector('.part-desc').textContent = selectedObject.userData.desc || '';
infoPanel.querySelector('.part-material').textContent = selectedObject.userData.material || '';
infoPanel.classList.add('visible');
} else {
selectedObject = null;
infoPanel.classList.remove('visible');
}
});
// ==================== 视图切换 ====================
let viewMode = 'normal';
let explodeProgress = 0;
let targetExplode = 0;
window.setView = function(mode) {
targetExplode = mode === 'explode' ? 1 : 0;
viewMode = mode;
document.getElementById('btn-normal').classList.toggle('active', mode === 'normal');
document.getElementById('btn-explode').classList.toggle('active', mode === 'explode');
};
window.toggleAutoRotate = function() {
controls.autoRotate = !controls.autoRotate;
document.getElementById('btn-auto').classList.toggle('active', controls.autoRotate);
};
window.resetCamera = function() {
const targetY = explodeProgress > 0.5 ? 3 : -0.5;
camera.position.set(18, 14 + targetY * 0.3, 28);
controls.target.set(0, targetY, 0);
controls.update();
};
// 初始状态
document.getElementById('btn-auto').classList.add('active');
// ==================== 粒子效果 ====================
const particlesGeom = new THREE.BufferGeometry();
const particlesCount = 80;
const particlesPos = new Float32Array(particlesCount * 3);
const particlesSizes = new Float32Array(particlesCount);
for (let i = 0; i < particlesCount; i++) {
const theta = Math.random() * Math.PI * 2;
const phi = Math.random() * Math.PI * 0.5;
const r = 22 + Math.random() * 35;
particlesPos[i * 3] = Math.cos(theta) * Math.cos(phi) * r;
particlesPos[i * 3 + 1] = 2 + Math.random() * 25;
particlesPos[i * 3 + 2] = Math.sin(theta) * Math.cos(phi) * r;
particlesSizes[i] = Math.random() * 0.04 + 0.02;
}
particlesGeom.setAttribute('position', new THREE.BufferAttribute(particlesPos, 3));
particlesGeom.setAttribute('size', new THREE.BufferAttribute(particlesSizes, 1));
const particlesMat = new THREE.PointsMaterial({
color: 0xc8a45a, size: 0.12, transparent: true, opacity: 0.5,
blending: THREE.AdditiveBlending, depthWrite: false,
});
const particles = new THREE.Points(particlesGeom, particlesMat);
scene.add(particles);
// ==================== 动画循环 ====================
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.1);
const elapsed = clock.elapsedTime;
controls.update();
// 爆炸视图过渡
explodeProgress += (targetExplode - explodeProgress) * Math.min(dt * 3.5, 1);
explodeGroups.forEach(eg => {
if (!eg.group) return;
const targetY = eg.normalY + (eg.explodeY - eg.normalY) * explodeProgress;
eg.group.position.y += (targetY - eg.group.position.y) * Math.min(dt * 4, 1);
});
// 额外组件
extraExplodeTargets.forEach(et => {
const ty = et.normalY + (et.explodeY - et.normalY) * explodeProgress;
et.group.position.y += (ty - et.group.position.y) * Math.min(dt * 4, 1);
});
// 摆轮振荡(4Hz ≈ 28,800 bph)
const balanceFreq = 4;
const balanceAmp = 0.35;
balanceGroup.rotation.y = Math.sin(elapsed * Math.PI * 2 * balanceFreq) * balanceAmp;
// 擒纵叉摆动
palletFork.rotation.y = Math.sin(elapsed * Math.PI * 2 * balanceFreq) * 0.18 + 0.05;
// 擒纵轮间歇转动
const tickPeriod = 1 / (balanceFreq * 2);
const tickPhase = (elapsed % tickPeriod) / tickPeriod;
const baseRotation = Math.floor(elapsed / tickPeriod) * (Math.PI * 2 / 15);
escapeWheel.rotation.y = baseRotation + Math.max(0, (0.08 - tickPhase) * 2) * 0.3;
// 齿轮缓慢旋转
const gearSpeed = 0.08;
centerWheel.rotation.y += dt * gearSpeed * 3;
thirdWheel.rotation.y -= dt * gearSpeed * 9;
fourthWheel.rotation.y += dt * gearSpeed * 27;
ratchetWheel.rotation.y += dt * gearSpeed * 0.3;
barrelOuter.rotation.y += dt * gearSpeed * 0.5;
// 指针旋转
const simHours = (elapsed * 0.02) % 12;
const simMinutes = (elapsed * 0.24) % 60;
const simSeconds = (elapsed * 14.4) % 60;
hourHand.rotation.y = -(simHours / 12) * Math.PI * 2;
minuteHand.rotation.y = -(simMinutes / 60) * Math.PI * 2;
secondHand.rotation.y = -(simSeconds / 60) * Math.PI * 2;
// 粒子动画
const posArr = particlesGeom.attributes.position.array;
for (let i = 0; i < particlesCount; i++) {
posArr[i * 3 + 1] += Math.sin(elapsed * 0.7 + i) * 0.008;
posArr[i * 3] += Math.cos(elapsed * 0.5 + i * 1.3) * 0.005;
}
particlesGeom.attributes.position.needsUpdate = true;
particles.rotation.y += dt * 0.04;
// 点光源微动
pointLights.forEach((pl, i) => {
pl.intensity = 25 + Math.sin(elapsed * 1.5 + i) * 8;
});
// 夜光动态亮度
const lumeGlow = 1.6 + Math.sin(elapsed * 0.6) * 0.4;
matLume.emissiveIntensity = lumeGlow;
renderer.render(scene, camera);
}
// ==================== 响应式 ====================
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 键盘快捷键
window.addEventListener('keydown', (e) => {
switch (e.key.toLowerCase()) {
case 'e': setView(viewMode === 'explode' ? 'normal' : 'explode'); break;
case 'r': resetCamera(); break;
case 'a': toggleAutoRotate(); break;
case 'f': camera.position.set(0, 0, 35); controls.target.set(0, -0.5, 0); controls.update(); break;
case 't': camera.position.set(0, 25, 2); controls.target.set(0, -0.5, 0); controls.update(); break;
}
});
// ==================== 启动 ====================
animate();
console.log('%c⌚ 3D镂空机械腕表已就绪 %c| %cCalibre Squelette',
'color:#c8a45a;font-size:16px;',
'',
'color:#888;');
console.log('%c操作提示: 拖拽旋转 | 滚轮缩放 | 右键平移 | E=爆炸视图 | A=自动旋转 | F=正面 | T=俯视 | 点击零件查看详情',
'color:#888;font-size:11px;');
</script>
</body>
</html>