"use client"; import { useEffect, useRef } from "react"; import * as THREE from "three"; export type TopicPrepVariant = "indie" | "aiagent"; type Props = { variant: TopicPrepVariant; }; function disposeObject3D(root: THREE.Object3D) { const materials = new Set(); const geometries = new Set(); root.traverse((object) => { if (object instanceof THREE.Mesh || object instanceof THREE.LineSegments) { const g = object.geometry; if (g && !geometries.has(g)) { geometries.add(g); g.dispose(); } const mat = object.material; if (Array.isArray(mat)) mat.forEach((m) => materials.add(m)); else if (mat) materials.add(mat); } }); materials.forEach((m) => m.dispose()); } /** 独立开发:居家书桌 + 显示器 + homelab 机架 */ function buildIndieDeskScene(root: THREE.Group) { const wood = new THREE.MeshStandardMaterial({ color: 0x6b5344, roughness: 0.88, metalness: 0.05, }); const deskDark = new THREE.MeshStandardMaterial({ color: 0x18181b, roughness: 0.55, metalness: 0.25, }); const screenGlow = new THREE.MeshStandardMaterial({ color: 0x052e16, emissive: 0x4ade80, emissiveIntensity: 0.55, roughness: 0.35, }); const keyCap = new THREE.MeshStandardMaterial({ color: 0x3f3f46, roughness: 0.45, metalness: 0.15, }); const rackLed = new THREE.MeshStandardMaterial({ color: 0x0c0a09, emissive: 0xf59e0b, emissiveIntensity: 0.5, roughness: 0.4, }); const floor = new THREE.Mesh(new THREE.PlaneGeometry(7, 5.5), wood); floor.rotation.x = -Math.PI / 2; floor.position.y = -1.15; root.add(floor); const rug = new THREE.Mesh(new THREE.PlaneGeometry(2.8, 2), new THREE.MeshStandardMaterial({ color: 0x3d3832, roughness: 0.95, })); rug.rotation.x = -Math.PI / 2; rug.position.set(0, -1.14, 0.1); root.add(rug); const deskTop = new THREE.Mesh(new THREE.BoxGeometry(2.4, 0.07, 1), deskDark); deskTop.position.set(0, -0.38, 0.15); root.add(deskTop); const legGeo = new THREE.BoxGeometry(0.08, 0.45, 0.08); const legPositions: [number, number, number][] = [ [-1, -0.65, -0.25], [1, -0.65, -0.25], [-1, -0.65, 0.55], [1, -0.65, 0.55], ]; for (const [x, y, z] of legPositions) { const leg = new THREE.Mesh(legGeo, deskDark); leg.position.set(x, y, z); root.add(leg); } const monitorBezel = new THREE.Mesh(new THREE.BoxGeometry(1, 0.62, 0.045), deskDark); monitorBezel.position.set(0, 0.12, -0.28); root.add(monitorBezel); const screen = new THREE.Mesh(new THREE.PlaneGeometry(0.88, 0.52), screenGlow); screen.position.set(0, 0.12, -0.28 + 0.023); root.add(screen); const stand = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.12, 0.15), deskDark); stand.position.set(0, -0.28, -0.22); root.add(stand); const keyboard = new THREE.Mesh(new THREE.BoxGeometry(0.75, 0.035, 0.3), keyCap); keyboard.position.set(0, -0.32, 0.38); keyboard.rotation.x = -0.06; root.add(keyboard); const mug = new THREE.Mesh( new THREE.CylinderGeometry(0.09, 0.08, 0.12, 16), new THREE.MeshStandardMaterial({ color: 0x3f3f46, roughness: 0.4 }), ); mug.position.set(-0.85, -0.31, 0.42); root.add(mug); const rack = new THREE.Mesh(new THREE.BoxGeometry(0.38, 0.95, 0.42), deskDark); rack.position.set(1.05, 0.1, -0.05); root.add(rack); const rackFace = new THREE.Mesh(new THREE.PlaneGeometry(0.3, 0.82), rackLed); rackFace.position.set(1.05 + 0.191, 0.1, -0.05); root.add(rackFace); for (let row = 0; row < 4; row++) { const led = new THREE.Mesh( new THREE.PlaneGeometry(0.22, 0.04), new THREE.MeshStandardMaterial({ color: 0x000000, emissive: row % 2 === 0 ? 0x22c55e : 0xf59e0b, emissiveIntensity: 0.65, }), ); led.position.set(1.05 + 0.192, 0.35 - row * 0.18, -0.05); root.add(led); } const miniServer = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.15, 0.18), deskDark); miniServer.position.set(1.05, -0.28, 0.45); root.add(miniServer); } /** AI Agent:赛博空间网格 + 机器人 + 霓虹光环 */ function buildAiCyberScene(root: THREE.Group) { const grid = new THREE.GridHelper(9, 28, 0x6366f1, 0x312e81); grid.position.y = -1.05; const gMat = grid.material; if (Array.isArray(gMat)) { gMat.forEach((m) => { if ("opacity" in m) { (m as THREE.Material & { opacity: number }).opacity = 0.45; m.transparent = true; } }); } else if (gMat && "opacity" in gMat) { (gMat as THREE.Material & { opacity: number }).opacity = 0.45; gMat.transparent = true; } root.add(grid); const bodyMat = new THREE.MeshStandardMaterial({ color: 0x52525b, metalness: 0.55, roughness: 0.28, flatShading: true, }); const glowCyan = new THREE.MeshStandardMaterial({ color: 0x06b6d4, emissive: 0x22d3ee, emissiveIntensity: 0.85, }); const glowMagenta = new THREE.MeshStandardMaterial({ color: 0xd946ef, emissive: 0xc026d3, emissiveIntensity: 0.55, }); const robot = new THREE.Group(); robot.position.y = -0.15; const torso = new THREE.Mesh(new THREE.BoxGeometry(0.52, 0.58, 0.32), bodyMat); torso.position.y = 0.1; robot.add(torso); const chestPanel = new THREE.Mesh(new THREE.PlaneGeometry(0.35, 0.25), glowCyan); chestPanel.position.set(0, 0.12, 0.161); robot.add(chestPanel); const head = new THREE.Mesh(new THREE.BoxGeometry(0.44, 0.36, 0.38), bodyMat); head.position.y = 0.55; robot.add(head); const visor = new THREE.Mesh(new THREE.BoxGeometry(0.38, 0.12, 0.02), glowCyan); visor.position.set(0, 0.52, 0.19); robot.add(visor); const eyeL = new THREE.Mesh(new THREE.SphereGeometry(0.055, 16, 16), glowCyan); eyeL.position.set(-0.1, 0.56, 0.18); const eyeR = new THREE.Mesh(new THREE.SphereGeometry(0.055, 16, 16), glowCyan); eyeR.position.set(0.1, 0.56, 0.18); robot.add(eyeL, eyeR); const antenna = new THREE.Mesh(new THREE.CylinderGeometry(0.025, 0.03, 0.28, 8), glowMagenta); antenna.position.set(0, 0.82, 0); robot.add(antenna); const armL = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.45, 0.12), bodyMat); armL.position.set(-0.38, 0.05, 0); const armR = armL.clone(); armR.position.x = 0.38; robot.add(armL, armR); root.add(robot); for (let i = 0; i < 3; i++) { const ringMat = new THREE.MeshBasicMaterial({ color: i === 1 ? 0xa855f7 : 0x22d3ee, transparent: true, opacity: 0.35 - i * 0.08, }); const ring = new THREE.Mesh(new THREE.TorusGeometry(0.95 + i * 0.22, 0.012, 8, 64), ringMat); ring.rotation.x = Math.PI / 2; ring.position.y = -0.85 + i * 0.15; root.add(ring); } const holo = new THREE.Mesh( new THREE.OctahedronGeometry(0.18, 0), new THREE.MeshBasicMaterial({ color: 0x67e8f9, wireframe: true, transparent: true, opacity: 0.5 }), ); holo.position.set(-0.55, 0.35, 0.35); root.add(holo); return { robot, holo }; } export function TopicPrepThreeScene({ variant }: Props) { const containerRef = useRef(null); const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; const container = containerRef.current; if (!canvas || !container) return; const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true, powerPreference: "high-performance", }); const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100); camera.position.set(0, 0.35, 5.2); const group = new THREE.Group(); scene.add(group); let bobRefs: { robot?: THREE.Group; holo?: THREE.Mesh } | null = null; if (variant === "indie") { scene.background = null; const ambient = new THREE.AmbientLight(0xfff5e6, 0.45); scene.add(ambient); const warm = new THREE.PointLight(0xffb86b, 1.1, 12); warm.position.set(0.6, 1.2, 1.2); scene.add(warm); const cool = new THREE.PointLight(0x4ade80, 0.35, 8); cool.position.set(-0.3, 0.5, 0.5); scene.add(cool); const dir = new THREE.DirectionalLight(0xffffff, 0.35); dir.position.set(-2, 4, 3); scene.add(dir); buildIndieDeskScene(group); group.rotation.y = 0.25; } else { scene.fog = new THREE.FogExp2(0x0a0a14, 0.055); const ambient = new THREE.AmbientLight(0x4c1d95, 0.35); scene.add(ambient); const key = new THREE.PointLight(0x22d3ee, 1.2, 16); key.position.set(1.5, 2, 2); scene.add(key); const rim = new THREE.PointLight(0xd946ef, 0.85, 14); rim.position.set(-2, 0.5, -1); scene.add(rim); const dir = new THREE.DirectionalLight(0xa78bfa, 0.4); dir.position.set(0, 3, 4); scene.add(dir); bobRefs = buildAiCyberScene(group); } let rx = 0.1; let ry = variant === "indie" ? 0.2 : 0; let vx = 0; let vy = 0; const sens = 0.0052; const damping = 0.93; const autoRy = variant === "indie" ? 0.0018 : 0.0024; let dragging = false; let lastX = 0; let lastY = 0; const onPointerDown = (e: PointerEvent) => { dragging = true; lastX = e.clientX; lastY = e.clientY; canvas.setPointerCapture(e.pointerId); }; const onPointerMove = (e: PointerEvent) => { if (!dragging) return; const dx = e.clientX - lastX; const dy = e.clientY - lastY; lastX = e.clientX; lastY = e.clientY; vy += dx * sens; vx += dy * sens; }; const onPointerEnd = (e: PointerEvent) => { dragging = false; try { canvas.releasePointerCapture(e.pointerId); } catch { /* already released */ } }; canvas.addEventListener("pointerdown", onPointerDown); canvas.addEventListener("pointermove", onPointerMove); canvas.addEventListener("pointerup", onPointerEnd); canvas.addEventListener("pointercancel", onPointerEnd); canvas.style.touchAction = "none"; const resize = () => { const w = Math.max(280, container.clientWidth); const h = Math.min(440, Math.max(236, Math.round(w * 0.5))); camera.aspect = w / h; camera.updateProjectionMatrix(); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.setSize(w, h, false); }; const ro = new ResizeObserver(resize); ro.observe(container); resize(); let raf = 0; const t0 = performance.now(); const tick = () => { raf = requestAnimationFrame(tick); const t = (performance.now() - t0) * 0.001; if (!dragging) vy += autoRy; rx += vx; ry += vy; vx *= damping; vy *= damping; group.rotation.x = rx; group.rotation.y = ry; if (bobRefs?.robot) { bobRefs.robot.position.y = -0.15 + Math.sin(t * 1.8) * 0.035; } if (bobRefs?.holo) { bobRefs.holo.rotation.y = t * 0.9; bobRefs.holo.rotation.x = t * 0.45; } renderer.render(scene, camera); }; tick(); return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.removeEventListener("pointerdown", onPointerDown); canvas.removeEventListener("pointermove", onPointerMove); canvas.removeEventListener("pointerup", onPointerEnd); canvas.removeEventListener("pointercancel", onPointerEnd); scene.fog = null; disposeObject3D(group); scene.remove(group); renderer.dispose(); }; }, [variant]); const label = variant === "indie" ? "独立开发:居家工作台与 homelab 预览" : "AI Agent:赛博空间与机器人预览"; const hint = variant === "indie" ? "拖动旋转 · 居家书桌、显示器与 homelab 机架(课程筹备视觉预告)" : "拖动旋转 · 赛博网格空间与机器人(课程筹备视觉预告)"; return (

{hint}

); }