Files
nagi-bench/outputs/gemini-3-1-pro/google-ai-studio-high/skeleton-watch.html
T
_‭⁧HachiMahiro哈基寻⁧‭ e166e31d92 feat(gem3.1proais):add Gemini 3.1 Pro AiStudio High skeleton-watch benchmark (#33)
* Add files via upload

* Update contributor information in JSON file

* Change contributor and update notes in JSON file

Updated contributor name and modified notes for clarity.
2026-07-13 14:04:30 +01:00

639 lines
30 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aethelgard 3D 镂空陀飞轮机械表</title>
<style>
:root {
--primary: #d4af37; /* Rose Gold / Brass */
--bg-color: #070709;
--panel-bg: rgba(15, 15, 18, 0.75);
}
body { margin: 0; overflow: hidden; background-color: var(--bg-color); color: #fff; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; user-select: none; }
#canvas-container { position: absolute; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 1; }
/* UI Layer */
#ui-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 10; pointer-events: none; display: flex; flex-direction: column; justify-content: space-between; box-sizing: border-box; padding: 40px; }
/* Header */
#header h1 { margin: 0; font-size: 32px; font-weight: 200; letter-spacing: 8px; text-transform: uppercase; color: var(--primary); }
#header p { margin: 8px 0 0 0; color: #888; font-size: 12px; letter-spacing: 3px; font-weight: 300; }
/* Info Panel */
#info-panel { position: absolute; top: 50%; right: -400px; transform: translateY(-50%); width: 340px; background: var(--panel-bg); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border: 1px solid rgba(255,255,255,0.05); border-radius: 12px; padding: 24px; transition: right 0.6s cubic-bezier(0.16, 1, 0.3, 1); box-shadow: 0 20px 40px rgba(0,0,0,0.5); pointer-events: auto; }
#info-panel.visible { right: 40px; }
#info-title { font-size: 22px; color: #fff; margin-bottom: 12px; font-weight: 300; letter-spacing: 1px; border-bottom: 1px solid rgba(212, 175, 55, 0.3); padding-bottom: 12px; }
#info-desc { font-size: 14px; color: #aaa; line-height: 1.6; font-weight: 300; }
/* Controls */
#controls { position: absolute; bottom: 40px; left: 50%; transform: translateX(-50%); pointer-events: auto; display: flex; gap: 20px; }
button { background: rgba(212, 175, 55, 0.05); border: 1px solid rgba(212, 175, 55, 0.5); color: var(--primary); padding: 12px 32px; font-size: 14px; letter-spacing: 3px; border-radius: 30px; cursor: pointer; transition: all 0.4s ease; text-transform: uppercase; outline: none; backdrop-filter: blur(5px); }
button:hover { background: var(--primary); color: #000; box-shadow: 0 0 20px rgba(212, 175, 55, 0.4); }
/* Tooltip */
#tooltip { position: absolute; background: rgba(0, 0, 0, 0.85); border: 1px solid rgba(212, 175, 55, 0.3); color: #fff; padding: 10px 16px; border-radius: 8px; font-size: 13px; font-weight: 300; pointer-events: none; opacity: 0; transition: opacity 0.2s; white-space: nowrap; z-index: 20; transform: translate(-50%, -150%); letter-spacing: 1px; backdrop-filter: blur(4px); }
/* Loading Screen */
#loader { position: absolute; top: 0; left: 0; width: 100vw; height: 100vh; background: var(--bg-color); z-index: 100; display: flex; justify-content: center; align-items: center; color: var(--primary); font-size: 20px; letter-spacing: 4px; font-weight: 200; transition: opacity 1s ease; }
</style>
<!-- 引入 Three.js -->
<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>
</head>
<body>
<div id="loader">CRAFTING CALIBER...</div>
<div id="tooltip"></div>
<div id="canvas-container"></div>
<div id="ui-layer">
<div id="header">
<h1>Aethelgard</h1>
<p>SKELETON TOURBILLON CHRONOMETER</p>
</div>
<div id="info-panel">
<div id="info-title">组件名称</div>
<div id="info-desc">组件详情...</div>
</div>
<div id="controls">
<button id="btn-explode">Toggle Exploded View</button>
</div>
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
import { Reflector } from 'three/addons/objects/Reflector.js';
// --- 全局变量与场景初始化 ---
let scene, camera, renderer, controls, pmremGenerator;
let masterGroup;
const watchParts = [];
let isExploded = false;
let explodeProgress = 0;
const clock = new THREE.Clock();
// 交互状态
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let hoveredPart = null;
let selectedPart = null;
const tooltip = document.getElementById('tooltip');
const infoPanel = document.getElementById('info-panel');
const infoTitle = document.getElementById('info-title');
const infoDesc = document.getElementById('info-desc');
// --- 核心材质库 (追求极致逼真) ---
const mats = {
gold: new THREE.MeshPhysicalMaterial({ color: 0xe5a073, metalness: 1.0, roughness: 0.15, clearcoat: 0.3 }),
brass: new THREE.MeshPhysicalMaterial({ color: 0xc5a059, metalness: 1.0, roughness: 0.25 }),
steel: new THREE.MeshPhysicalMaterial({ color: 0xdddddd, metalness: 1.0, roughness: 0.15 }),
darkSteel: new THREE.MeshPhysicalMaterial({ color: 0x333333, metalness: 1.0, roughness: 0.3 }),
brushed: new THREE.MeshPhysicalMaterial({ color: 0xaaaaaa, metalness: 1.0, roughness: 0.4 }),
ruby: new THREE.MeshPhysicalMaterial({ color: 0xff0033, metalness: 0.1, roughness: 0, transmission: 0.95, ior: 1.76, thickness: 0.5, clearcoat: 1.0 }),
glass: new THREE.MeshPhysicalMaterial({ color: 0xffffff, metalness: 0, roughness: 0, transmission: 1.0, ior: 1.5, thickness: 0.2, transparent: true }),
lume: new THREE.MeshStandardMaterial({ color: 0xddffdd, emissive: 0x44ff44, emissiveIntensity: 2.0 }),
blackCarbon: new THREE.MeshPhysicalMaterial({ color: 0x111111, metalness: 0.8, roughness: 0.6, clearcoat: 0.2 })
};
// --- 程序化生成 Canvas 纹理 ---
function createPerlageTexture() {
const canvas = document.createElement('canvas');
canvas.width = 512; canvas.height = 512;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#999'; ctx.fillRect(0, 0, 512, 512);
for (let y = -20; y <= 530; y += 24) {
for (let x = -20; x <= 530; x += 24) {
const offset = (y / 24) % 2 === 0 ? 0 : 12;
const cx = x + offset;
const grad = ctx.createRadialGradient(cx, y, 0, cx, y, 16);
grad.addColorStop(0, 'rgba(255,255,255,0.4)');
grad.addColorStop(0.7, 'rgba(100,100,100,0.1)');
grad.addColorStop(1, 'rgba(0,0,0,0.2)');
ctx.fillStyle = grad;
ctx.beginPath(); ctx.arc(cx, y, 16, 0, Math.PI * 2); ctx.fill();
}
}
const tex = new THREE.CanvasTexture(canvas);
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(2, 2);
return tex;
}
function createSunburstTexture() {
const canvas = document.createElement('canvas');
canvas.width = 512; canvas.height = 512;
const ctx = canvas.getContext('2d');
const cx = 256, cy = 256;
ctx.fillStyle = '#888'; ctx.fillRect(0, 0, 512, 512);
for (let i = 0; i < 360; i += 2) {
ctx.beginPath();
ctx.moveTo(cx, cy);
const angle = i * Math.PI / 180;
ctx.lineTo(cx + Math.cos(angle) * 300, cy + Math.sin(angle) * 300);
ctx.strokeStyle = i % 4 === 0 ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.1)';
ctx.lineWidth = 2;
ctx.stroke();
}
return new THREE.CanvasTexture(canvas);
}
// --- 几何体构建辅助函数 ---
function createGearGeometry(radius, teeth, innerHole) {
const shape = new THREE.Shape();
const teethDepth = radius * 0.12;
const outerR = radius;
const innerR = radius - teethDepth;
for (let i = 0; i < teeth * 2; i++) {
const r = i % 2 === 0 ? outerR : innerR;
const a = (i / (teeth * 2)) * Math.PI * 2;
if (i === 0) shape.moveTo(Math.cos(a) * r, Math.sin(a) * r);
else shape.lineTo(Math.cos(a) * r, Math.sin(a) * r);
}
shape.closePath();
if (innerHole > 0) {
const hole = new THREE.Path();
hole.absarc(0, 0, innerHole, 0, Math.PI * 2, false);
shape.holes.push(hole);
}
return new THREE.ExtrudeGeometry(shape, { depth: 0.05, bevelEnabled: true, bevelThickness: 0.01, bevelSize: 0.01, bevelSegments: 1, curveSegments: 4 });
}
// 游丝螺旋曲线
class SpiralCurve extends THREE.Curve {
getPoint(t, optionalTarget = new THREE.Vector3()) {
const angle = t * Math.PI * 2 * 10; // 10 圈
const r = 0.05 + t * 0.55;
return optionalTarget.set(Math.cos(angle) * r, Math.sin(angle) * r, 0);
}
}
// --- WatchPart 类:封装交互与高亮 ---
class WatchPart extends THREE.Group {
constructor(name, desc, layerZ) {
super();
this.partName = name;
this.partDesc = desc;
this.layerZ = layerZ; // 爆炸视图的 Z 轴目标
this.isInteractive = true;
this.meshes = [];
watchParts.push(this);
}
addMesh(mesh) {
mesh.material = mesh.material.clone(); // 独立材质以便高亮
mesh.userData.originalEmissive = mesh.material.emissive ? mesh.material.emissive.clone() : new THREE.Color(0x000000);
this.meshes.push(mesh);
this.add(mesh);
}
setHighlight(active) {
this.meshes.forEach(m => {
if (m.material.emissive) {
if (active) {
m.material.emissive.setHex(0x333333);
} else {
m.material.emissive.copy(m.userData.originalEmissive);
}
}
});
}
}
// 添加螺丝与红宝石细节
function addRubyAndScrew(group, x, y, z) {
const ruby = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.12, 0.06, 16), mats.ruby);
ruby.position.set(x, y, z);
ruby.rotation.x = Math.PI / 2;
group.addMesh(ruby);
const screwGeo = new THREE.CylinderGeometry(0.08, 0.08, 0.08, 16);
const screw = new THREE.Mesh(screwGeo, mats.steel);
screw.position.set(x + 0.3, y + 0.3, z);
screw.rotation.x = Math.PI / 2;
group.addMesh(screw);
}
// ================= 构建腕表 =================
let balanceGroup, escapeGroup, secondHand, minuteHand, hourHand;
let barrelGroup, centerGroup, thirdGroup, fourthGroup;
function buildWatch() {
masterGroup = new THREE.Group();
scene.add(masterGroup);
// 层级定义 (Z坐标偏移量,爆炸视图时使用)
const Z_GLASS = 5.0;
const Z_HANDS = 3.0;
const Z_DIAL = 2.0;
const Z_BRIDGES = 1.0;
const Z_GEARS = 0.0;
const Z_BASEPLATE = -1.5;
const Z_CASEBACK = -3.0;
// 1. 蓝宝石表镜 (Sapphire Crystal)
const glassPart = new WatchPart("Sapphire Crystal", "Anti-reflective domed sapphire crystal, extremely scratch-resistant.", Z_GLASS);
const glassGeo = new THREE.CylinderGeometry(4.8, 4.8, 0.1, 64);
const glassMesh = new THREE.Mesh(glassGeo, mats.glass);
glassMesh.position.z = 1.2;
glassMesh.rotation.x = Math.PI / 2;
glassPart.addMesh(glassMesh);
masterGroup.add(glassPart);
// 2. 表壳与表圈 (Case & Bezel)
const casePart = new WatchPart("Titanium Case", "Aerospace-grade titanium alloy case with brushed finishing and forged carbon bezel.", 0); // 表壳不移动
const caseBody = new THREE.Mesh(new THREE.CylinderGeometry(5.2, 5.2, 2.0, 64), mats.darkSteel);
caseBody.rotation.x = Math.PI / 2;
casePart.addMesh(caseBody);
const bezel = new THREE.Mesh(new THREE.TorusGeometry(4.9, 0.3, 32, 64), mats.blackCarbon);
bezel.position.z = 1.0;
casePart.addMesh(bezel);
// 表耳
const lugGeo = new THREE.BoxGeometry(1.2, 2.5, 1.8);
for(let i=0; i<4; i++) {
const lug = new THREE.Mesh(lugGeo, mats.darkSteel);
const signX = (i%2 === 0) ? 1 : -1;
const signY = (i<2) ? 1 : -1;
lug.position.set(3.2 * signX, 4.8 * signY, 0);
lug.rotation.z = -0.4 * signX * signY;
casePart.addMesh(lug);
}
masterGroup.add(casePart);
// 3. 镂空表盘与刻度 (Skeleton Dial)
const dialPart = new WatchPart("Skeleton Dial", "Floating chapter ring with applied luminescent indices.", Z_DIAL);
const ringShape = new THREE.Shape();
ringShape.absarc(0, 0, 4.7, 0, Math.PI*2, false);
const ringHole = new THREE.Path();
ringHole.absarc(0, 0, 4.0, 0, Math.PI*2, false);
ringShape.holes.push(ringHole);
const dialRing = new THREE.Mesh(new THREE.ExtrudeGeometry(ringShape, {depth: 0.1, bevelEnabled: true, bevelThickness: 0.02, bevelSize: 0.02}), mats.darkSteel);
dialRing.position.z = 0.6;
dialPart.addMesh(dialRing);
for(let i=0; i<12; i++) {
const angle = (i / 12) * Math.PI * 2;
const marker = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.6, 0.1), mats.lume);
marker.position.set(Math.cos(angle)*4.3, Math.sin(angle)*4.3, 0.7);
marker.rotation.z = angle + Math.PI/2;
dialPart.addMesh(marker);
}
masterGroup.add(dialPart);
// 4. 指针 (Hands)
const handsPart = new WatchPart("Hands", "Sword-style hour and minute hands with Super-LumiNova filling.", Z_HANDS);
// 时针
hourHand = new THREE.Group();
const hShape = new THREE.Shape();
hShape.moveTo(0, -0.4); hShape.lineTo(0.2, 0); hShape.lineTo(0, 2.5); hShape.lineTo(-0.2, 0);
const hMesh = new THREE.Mesh(new THREE.ExtrudeGeometry(hShape, {depth: 0.05, bevelEnabled:true, bevelSize:0.02, bevelThickness:0.02}), mats.steel);
const hLume = new THREE.Mesh(new THREE.BoxGeometry(0.1, 1.8, 0.06), mats.lume);
hLume.position.set(0, 1.2, 0.02);
hourHand.add(hMesh, hLume);
hourHand.position.z = 0.8;
// 分针
minuteHand = new THREE.Group();
const mShape = new THREE.Shape();
mShape.moveTo(0, -0.5); mShape.lineTo(0.15, 0); mShape.lineTo(0, 3.8); mShape.lineTo(-0.15, 0);
const mMesh = new THREE.Mesh(new THREE.ExtrudeGeometry(mShape, {depth: 0.05, bevelEnabled:true, bevelSize:0.02, bevelThickness:0.02}), mats.steel);
const mLume = new THREE.Mesh(new THREE.BoxGeometry(0.08, 2.8, 0.06), mats.lume);
mLume.position.set(0, 1.8, 0.02);
minuteHand.add(mMesh, mLume);
minuteHand.position.z = 0.9;
// 秒针
secondHand = new THREE.Mesh(new THREE.BoxGeometry(0.04, 4.5, 0.02), mats.ruby); // 红漆秒针
secondHand.geometry.translate(0, 1.5, 0);
secondHand.position.z = 1.0;
handsPart.addMesh(hourHand.children[0]); handsPart.addMesh(minuteHand.children[0]); handsPart.addMesh(secondHand);
handsPart.add(hourHand, minuteHand, secondHand);
masterGroup.add(handsPart);
// 5. 夹板 (Bridges)
const bridgesPart = new WatchPart("Bridges", "Hand-beveled bridges securing the gear train, finished with straight graining.", Z_BRIDGES);
// 发条盒夹板
const bridge1 = new THREE.Shape();
bridge1.absarc(1.5, 1.5, 1.8, 0, Math.PI*2);
const b1Mesh = new THREE.Mesh(new THREE.ExtrudeGeometry(bridge1, {depth: 0.1, bevelEnabled: true}), mats.brushed);
b1Mesh.position.z = 0.4;
bridgesPart.addMesh(b1Mesh);
addRubyAndScrew(bridgesPart, 1.5, 1.5, 0.45);
// 传动轮夹板
const bridge2 = new THREE.Shape();
bridge2.moveTo(-0.5, 0.5);
bridge2.quadraticCurveTo(-2, 1, -2.5, -0.5);
bridge2.quadraticCurveTo(-1.5, -2.5, 0, -1);
bridge2.quadraticCurveTo(0, 0, -0.5, 0.5);
const b2Mesh = new THREE.Mesh(new THREE.ExtrudeGeometry(bridge2, {depth: 0.1, bevelEnabled: true}), mats.brushed);
b2Mesh.position.z = 0.4;
bridgesPart.addMesh(b2Mesh);
addRubyAndScrew(bridgesPart, 0, 0, 0.45); // Center wheel ruby
addRubyAndScrew(bridgesPart, -1.2, -0.8, 0.45); // Third wheel ruby
// 摆轮夹板 (Balance Cock)
const bridge3 = new THREE.Shape();
bridge3.moveTo(3, -1);
bridge3.quadraticCurveTo(2.5, -3, 1, -2);
bridge3.lineTo(2, -1);
const b3Mesh = new THREE.Mesh(new THREE.ExtrudeGeometry(bridge3, {depth: 0.1, bevelEnabled: true}), mats.brushed);
b3Mesh.position.z = 0.4;
bridgesPart.addMesh(b3Mesh);
addRubyAndScrew(bridgesPart, 1.8, -1.8, 0.45); // Balance shock ruby
masterGroup.add(bridgesPart);
// 6. 传动系统 (Gear Train)
const gearsPart = new WatchPart("Gear Train & Escapement", "Transmits power from the mainspring to the escapement.", Z_GEARS);
// 发条盒 (Barrel)
barrelGroup = new THREE.Group();
const barrel = new THREE.Mesh(new THREE.CylinderGeometry(1.6, 1.6, 0.3, 32), mats.gold);
barrel.rotation.x = Math.PI/2;
const sunburstMat = mats.gold.clone();
sunburstMat.map = createSunburstTexture();
barrel.material = sunburstMat;
barrelGroup.add(barrel);
const barrelTeeth = new THREE.Mesh(createGearGeometry(1.7, 40, 1.5), mats.steel);
barrelGroup.add(barrelTeeth);
barrelGroup.position.set(1.5, 1.5, 0);
gearsPart.addMesh(barrel); gearsPart.addMesh(barrelTeeth);
gearsPart.add(barrelGroup);
// 中心轮 (Center Wheel)
centerGroup = new THREE.Group();
const cGear = new THREE.Mesh(createGearGeometry(1.2, 30, 1.0), mats.gold);
centerGroup.add(cGear);
for(let i=0; i<5; i++) {
const spoke = new THREE.Mesh(new THREE.BoxGeometry(2.2, 0.05, 0.05), mats.gold);
spoke.rotation.z = (i * Math.PI) / 5;
centerGroup.add(spoke);
}
centerGroup.position.set(0, 0, 0.1);
gearsPart.addMesh(cGear); gearsPart.add(centerGroup);
// 三轮 (Third Wheel)
thirdGroup = new THREE.Group();
const tGear = new THREE.Mesh(createGearGeometry(0.9, 24, 0.7), mats.brass);
thirdGroup.add(tGear);
for(let i=0; i<4; i++) {
const spoke = new THREE.Mesh(new THREE.BoxGeometry(1.6, 0.05, 0.05), mats.brass);
spoke.rotation.z = (i * Math.PI) / 4;
thirdGroup.add(spoke);
}
thirdGroup.position.set(-1.2, -0.8, 0.2);
gearsPart.addMesh(tGear); gearsPart.add(thirdGroup);
// 擒纵轮 (Escape Wheel)
escapeGroup = new THREE.Group();
const eGear = new THREE.Mesh(createGearGeometry(0.5, 15, 0.3), mats.steel);
escapeGroup.add(eGear);
escapeGroup.position.set(0.2, -1.5, 0.2);
gearsPart.addMesh(eGear); gearsPart.add(escapeGroup);
// 摆轮与游丝 (Balance Wheel & Hairspring)
balanceGroup = new THREE.Group();
const balRim = new THREE.Mesh(new THREE.TorusGeometry(0.9, 0.06, 16, 64), mats.gold);
balanceGroup.add(balRim);
const balSpoke = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.06, 0.04), mats.gold);
balanceGroup.add(balSpoke);
// 游丝
const hairspring = new THREE.Mesh(new THREE.TubeGeometry(new SpiralCurve(), 100, 0.015, 8, false), mats.steel);
hairspring.position.z = 0.15;
balanceGroup.add(hairspring);
balanceGroup.position.set(1.8, -1.8, 0.1);
gearsPart.addMesh(balRim); gearsPart.addMesh(hairspring); gearsPart.add(balanceGroup);
masterGroup.add(gearsPart);
// 7. 主夹板 (Baseplate)
const basePart = new WatchPart("Main Plate", "The foundation of the movement, decorated with Perlage (circular graining).", Z_BASEPLATE);
const baseShape = new THREE.Shape();
baseShape.absarc(0, 0, 4.6, 0, Math.PI*2);
// 切割出镂空区域
const cutout1 = new THREE.Path(); cutout1.absarc(1.5, 1.5, 1.2, 0, Math.PI*2); baseShape.holes.push(cutout1);
const cutout2 = new THREE.Path(); cutout2.absarc(-1.2, -0.8, 0.7, 0, Math.PI*2); baseShape.holes.push(cutout2);
const cutout3 = new THREE.Path(); cutout3.absarc(1.8, -1.8, 1.0, 0, Math.PI*2); baseShape.holes.push(cutout3);
const baseplateGeo = new THREE.ExtrudeGeometry(baseShape, {depth: 0.2, bevelEnabled: true, bevelThickness: 0.02});
const baseplateMat = mats.brushed.clone();
baseplateMat.map = createPerlageTexture();
const baseMesh = new THREE.Mesh(baseplateGeo, baseplateMat);
baseMesh.position.z = -0.2;
basePart.addMesh(baseMesh);
masterGroup.add(basePart);
// 8. 蓝宝石底盖 (Case Back)
const backPart = new WatchPart("Exhibition Case Back", "Sapphire crystal back displaying the movement.", Z_CASEBACK);
const backGlass = new THREE.Mesh(new THREE.CylinderGeometry(4.4, 4.4, 0.1, 64), mats.glass);
backGlass.rotation.x = Math.PI / 2;
backGlass.position.z = -0.5;
backPart.addMesh(backGlass);
masterGroup.add(backPart);
// 将整个手表放平 (Z轴朝上)
masterGroup.rotation.x = -Math.PI / 2;
}
// --- 初始化场景 ---
function init() {
const container = document.getElementById('canvas-container');
// 场景
scene = new THREE.Scene();
// 相机
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 10, 12);
// 渲染器 (追求极致画质)
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
container.appendChild(renderer.domElement);
// 光照与环境反射 (Photorealistic)
pmremGenerator = new THREE.PMREMGenerator(renderer);
pmremGenerator.compileEquirectangularShader();
scene.environment = pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture;
const spotLight = new THREE.SpotLight(0xffffff, 100);
spotLight.position.set(5, 15, 5);
spotLight.angle = Math.PI/6;
spotLight.penumbra = 0.5;
scene.add(spotLight);
// 地面镜面反射 (Showcase style)
const mirrorGeo = new THREE.PlaneGeometry(100, 100);
const mirror = new Reflector(mirrorGeo, {
clipBias: 0.003,
textureWidth: window.innerWidth * window.devicePixelRatio,
textureHeight: window.innerHeight * window.devicePixelRatio,
color: 0x111111
});
mirror.rotation.x = -Math.PI / 2;
mirror.position.y = -3;
scene.add(mirror);
// 构建手表
buildWatch();
// 控制器
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.maxPolarAngle = Math.PI / 2 + 0.1; // 不允许看穿地板
controls.minDistance = 6;
controls.maxDistance = 25;
controls.target.set(0, 0, 0);
// 事件监听
window.addEventListener('resize', onWindowResize);
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('click', onClick);
document.getElementById('btn-explode').addEventListener('click', () => {
isExploded = !isExploded;
});
// 移除加载屏
setTimeout(() => {
const loader = document.getElementById('loader');
loader.style.opacity = '0';
setTimeout(() => loader.remove(), 1000);
}, 500);
// 开始动画
renderer.setAnimationLoop(animate);
}
// --- 交互逻辑 ---
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
// 收集所有可交互网格
const interactables = [];
watchParts.forEach(p => interactables.push(...p.meshes));
const intersects = raycaster.intersectObjects(interactables, false);
if (intersects.length > 0) {
document.body.style.cursor = 'pointer';
// 查找所属的 WatchPart
let object = intersects[0].object;
let part = watchParts.find(p => p.meshes.includes(object));
if (part && hoveredPart !== part) {
if (hoveredPart) hoveredPart.setHighlight(false);
hoveredPart = part;
hoveredPart.setHighlight(true);
}
// 更新 Tooltip
tooltip.innerHTML = part.partName;
tooltip.style.opacity = 1;
tooltip.style.left = event.clientX + 'px';
tooltip.style.top = event.clientY + 'px';
} else {
document.body.style.cursor = 'default';
if (hoveredPart) {
hoveredPart.setHighlight(false);
hoveredPart = null;
}
tooltip.style.opacity = 0;
}
}
function onClick() {
if (hoveredPart) {
selectedPart = hoveredPart;
infoTitle.innerText = selectedPart.partName;
infoDesc.innerText = selectedPart.partDesc;
infoPanel.classList.add('visible');
} else {
infoPanel.classList.remove('visible');
}
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// --- 动画循环 ---
function animate() {
const time = clock.getElapsedTime();
const delta = clock.getDelta();
// 机械表动画 (28,800 vph = 4Hz = 8 ticks/sec)
const tickSpeed = 8;
const tick = Math.floor(time * tickSpeed);
const smoothTick = (time * tickSpeed) % 1;
if (balanceGroup) {
// 摆轮快速震荡
balanceGroup.rotation.z = Math.sin(time * 25) * 1.8;
}
if (escapeGroup) {
// 擒纵轮步进
escapeGroup.rotation.z = -(tick + (smoothTick < 0.2 ? smoothTick * 5 : 1)) * (Math.PI * 2 / 15);
}
if (secondHand) {
// 秒针扫秒
secondHand.rotation.z = -time * (Math.PI * 2 / 60);
}
if (minuteHand) {
minuteHand.rotation.z = -time * (Math.PI * 2 / 3600) * 60; // 加速演示
}
if (hourHand) {
hourHand.rotation.z = -time * (Math.PI * 2 / 43200) * 60;
}
if (barrelGroup) barrelGroup.rotation.z = time * 0.2;
if (centerGroup) centerGroup.rotation.z = time * 0.8;
if (thirdGroup) thirdGroup.rotation.z = -time * 1.5;
// 爆炸视图平滑插值
const targetProg = isExploded ? 1 : 0;
explodeProgress += (targetProg - explodeProgress) * 0.05;
watchParts.forEach(part => {
// 根据物体的局部坐标系进行位移 (因为 masterGroup 旋转了 -90度,Z 轴现在是垂直表盘的方向)
part.position.z = THREE.MathUtils.lerp(0, part.layerZ, explodeProgress);
});
// 整个腕表随时间缓慢呼吸式漂浮 (增加奢华感)
if (masterGroup && !isExploded) {
masterGroup.position.y = Math.sin(time * 0.5) * 0.1;
}
controls.update();
renderer.render(scene, camera);
}
// 启动
init();
</script>
</body>
</html>