"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()); } function applyFilmicRenderer(renderer: THREE.WebGLRenderer, exposure: number) { renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = exposure; } /** 独立开发:三面墙+顶+地 半封闭小房间(朝镜头开口),萌系配色与小狗 */ function buildIndieEnclosedRoom(root: THREE.Group): { dog: THREE.Group; tail: THREE.Mesh } { const wall = new THREE.MeshStandardMaterial({ color: 0xfff0f3, roughness: 0.94, metalness: 0, }); const floorMat = new THREE.MeshStandardMaterial({ color: 0xf5e0d4, roughness: 0.88, metalness: 0, }); const ceilMat = new THREE.MeshStandardMaterial({ color: 0xfff7fb, roughness: 0.92, metalness: 0, }); const floor = new THREE.Mesh(new THREE.PlaneGeometry(5.2, 4.2), floorMat); floor.rotation.x = -Math.PI / 2; floor.position.y = -1; root.add(floor); const back = new THREE.Mesh(new THREE.PlaneGeometry(5.2, 3.2), wall); back.position.set(0, 0.6, -2.1); root.add(back); const left = new THREE.Mesh(new THREE.PlaneGeometry(4.2, 3.2), wall); left.rotation.y = Math.PI / 2; left.position.set(-2.6, 0.6, 0); root.add(left); const right = new THREE.Mesh(new THREE.PlaneGeometry(4.2, 3.2), wall); right.rotation.y = -Math.PI / 2; right.position.set(2.6, 0.6, 0); root.add(right); const ceiling = new THREE.Mesh(new THREE.PlaneGeometry(5.2, 4.2), ceilMat); ceiling.rotation.x = Math.PI / 2; ceiling.position.y = 2.2; root.add(ceiling); const lamp = new THREE.Mesh( new THREE.PlaneGeometry(1.4, 0.9), new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0xfff1f2, emissiveIntensity: 0.55, roughness: 1, }), ); lamp.rotation.x = Math.PI / 2; lamp.position.set(0, 2.18, 0.2); root.add(lamp); const deskMat = new THREE.MeshStandardMaterial({ color: 0xf5e6d8, roughness: 0.5, metalness: 0.04, }); const deskTop = new THREE.Mesh(new THREE.BoxGeometry(1.65, 0.055, 0.72), deskMat); deskTop.position.set(-0.25, -0.45, 0.05); root.add(deskTop); const legGeo = new THREE.CylinderGeometry(0.038, 0.042, 0.36, 14); const legMat = new THREE.MeshStandardMaterial({ color: 0xe8d5c4, roughness: 0.58 }); for (const [x, z] of [ [-0.72, -0.22], [0.22, -0.22], [-0.72, 0.38], [0.22, 0.38], ]) { const leg = new THREE.Mesh(legGeo, legMat); leg.position.set(x, -0.66, z); root.add(leg); } const bezel = new THREE.MeshStandardMaterial({ color: 0x3f3f46, roughness: 0.42, metalness: 0.12, }); const monitor = new THREE.Mesh(new THREE.BoxGeometry(0.62, 0.42, 0.028), bezel); monitor.position.set(-0.25, -0.08, -0.38); root.add(monitor); const screenMat = new THREE.MeshStandardMaterial({ color: 0xa5d8ff, emissive: 0xc5eaff, emissiveIntensity: 0.28, roughness: 0.4, }); const screen = new THREE.Mesh(new THREE.PlaneGeometry(0.56, 0.36), screenMat); screen.position.set(-0.25, -0.08, -0.38 + 0.015); root.add(screen); const kb = new THREE.Mesh( new THREE.BoxGeometry(0.48, 0.022, 0.18), new THREE.MeshStandardMaterial({ color: 0xa8a29e, roughness: 0.55 }), ); kb.position.set(-0.25, -0.4, 0.22); kb.rotation.x = -0.035; root.add(kb); const tower = new THREE.Mesh( new THREE.BoxGeometry(0.18, 0.36, 0.32), new THREE.MeshStandardMaterial({ color: 0xfafafa, roughness: 0.32 }), ); tower.position.set(0.38, -0.38, -0.28); root.add(tower); const chairSeat = new THREE.Mesh( new THREE.BoxGeometry(0.4, 0.055, 0.36), new THREE.MeshStandardMaterial({ color: 0x78716c, roughness: 0.72 }), ); chairSeat.position.set(-0.25, -0.72, 0.55); root.add(chairSeat); const fur = new THREE.MeshStandardMaterial({ color: 0xffc4a8, roughness: 0.82, metalness: 0, }); const furDeep = new THREE.MeshStandardMaterial({ color: 0xf59e7c, roughness: 0.85, metalness: 0, }); const dog = new THREE.Group(); dog.position.set(0.75, -0.86, 0.22); const body = new THREE.Mesh(new THREE.SphereGeometry(0.2, 22, 18), fur); body.scale.set(1.05, 0.78, 1.12); body.position.y = 0.11; dog.add(body); const head = new THREE.Mesh(new THREE.SphereGeometry(0.16, 20, 18), fur); head.position.set(0, 0.3, 0.1); dog.add(head); const snout = new THREE.Mesh(new THREE.SphereGeometry(0.065, 14, 14), furDeep); snout.position.set(0, 0.26, 0.22); snout.scale.set(1, 0.88, 1.15); dog.add(snout); const nose = new THREE.Mesh( new THREE.SphereGeometry(0.022, 10, 10), new THREE.MeshStandardMaterial({ color: 0x292524, roughness: 0.35 }), ); nose.position.set(0, 0.28, 0.3); dog.add(nose); const eyeWhiteL = new THREE.Mesh( new THREE.SphereGeometry(0.045, 12, 12), new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.25 }), ); eyeWhiteL.position.set(-0.055, 0.32, 0.18); const eyeWhiteR = eyeWhiteL.clone(); eyeWhiteR.position.x = 0.055; dog.add(eyeWhiteL, eyeWhiteR); const pupilL = new THREE.Mesh( new THREE.SphereGeometry(0.022, 10, 10), new THREE.MeshStandardMaterial({ color: 0x1c1917, roughness: 0.2 }), ); pupilL.position.set(-0.052, 0.32, 0.2); const pupilR = pupilL.clone(); pupilR.position.x = 0.052; dog.add(pupilL, pupilR); const blushMat = new THREE.MeshStandardMaterial({ color: 0xff9eb5, roughness: 0.6, emissive: 0xff8fab, emissiveIntensity: 0.12, }); const blushL = new THREE.Mesh(new THREE.SphereGeometry(0.035, 10, 10), blushMat); blushL.scale.set(1.2, 0.45, 0.6); blushL.position.set(-0.1, 0.24, 0.16); const blushR = blushL.clone(); blushR.position.x = 0.1; dog.add(blushL, blushR); const earL = new THREE.Mesh(new THREE.SphereGeometry(0.055, 12, 12), furDeep); earL.position.set(-0.11, 0.36, 0.05); earL.scale.set(0.45, 0.95, 0.4); const earR = earL.clone(); earR.position.x = 0.11; dog.add(earL, earR); const tail = new THREE.Mesh( new THREE.CylinderGeometry(0.028, 0.036, 0.2, 12), furDeep, ); tail.rotation.z = Math.PI / 2.8; tail.rotation.x = 0.25; tail.position.set(-0.18, 0.16, -0.06); dog.add(tail); for (const [x, z] of [ [-0.09, 0.07], [0.09, 0.07], [-0.07, -0.07], [0.07, -0.07], ]) { const paw = new THREE.Mesh(new THREE.SphereGeometry(0.048, 12, 12), furDeep); paw.scale.set(1, 0.58, 1); paw.position.set(x, 0, z); dog.add(paw); } root.add(dog); return { dog, tail }; } /** AI:赛博空间,无网格线;萌系圆润机器人 + 实体光晕环(非线框) */ function buildAiCuteCyber(root: THREE.Group): { bot: THREE.Group; halo: THREE.Mesh; orbs: THREE.Mesh[]; } { const backdrop = new THREE.Mesh( new THREE.SphereGeometry(16, 48, 48), new THREE.MeshBasicMaterial({ color: 0x0c0618, side: THREE.BackSide }), ); root.add(backdrop); const floor = new THREE.Mesh( new THREE.CircleGeometry(4.5, 64), new THREE.MeshStandardMaterial({ color: 0x1e1435, roughness: 0.55, metalness: 0.15, emissive: 0x4c1d95, emissiveIntensity: 0.06, }), ); floor.rotation.x = -Math.PI / 2; floor.position.y = -1.05; root.add(floor); const haloMat = new THREE.MeshStandardMaterial({ color: 0xa78bfa, emissive: 0x8b5cf6, emissiveIntensity: 0.25, roughness: 0.35, metalness: 0.25, transparent: true, opacity: 0.55, side: THREE.DoubleSide, }); const halo = new THREE.Mesh(new THREE.TorusGeometry(1.05, 0.035, 16, 64), haloMat); halo.rotation.x = Math.PI / 2; halo.position.y = -0.35; root.add(halo); const shell = new THREE.MeshStandardMaterial({ color: 0xe8e0ff, roughness: 0.42, metalness: 0.18, }); const shellDark = new THREE.MeshStandardMaterial({ color: 0xc4b5fd, roughness: 0.45, metalness: 0.22, }); const eyeGlow = new THREE.MeshStandardMaterial({ color: 0x67e8f9, emissive: 0x22d3ee, emissiveIntensity: 0.35, roughness: 0.28, }); const bot = new THREE.Group(); bot.position.set(0, -0.25, 0); const body = new THREE.Mesh(new THREE.SphereGeometry(0.32, 28, 24), shell); body.scale.set(1, 0.88, 0.92); body.position.y = 0.05; bot.add(body); const tummy = new THREE.Mesh(new THREE.CircleGeometry(0.18, 24), shellDark); tummy.rotation.x = -0.15; tummy.position.set(0, 0.02, 0.27); bot.add(tummy); const head = new THREE.Mesh(new THREE.SphereGeometry(0.26, 28, 24), shell); head.position.y = 0.48; bot.add(head); const eyeWL = new THREE.Mesh(new THREE.SphereGeometry(0.07, 14, 14), new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.2 })); eyeWL.position.set(-0.08, 0.5, 0.22); const eyeWR = eyeWL.clone(); eyeWR.position.x = 0.08; bot.add(eyeWL, eyeWR); const eyeL = new THREE.Mesh(new THREE.SphereGeometry(0.038, 12, 12), eyeGlow); eyeL.position.set(-0.078, 0.5, 0.26); const eyeR = eyeL.clone(); eyeR.position.x = 0.078; bot.add(eyeL, eyeR); const cheek = new THREE.MeshStandardMaterial({ color: 0xf9a8d4, emissive: 0xf472b6, emissiveIntensity: 0.15, roughness: 0.5, }); const chL = new THREE.Mesh(new THREE.SphereGeometry(0.04, 10, 10), cheek); chL.scale.set(1.3, 0.5, 0.5); chL.position.set(-0.14, 0.42, 0.2); const chR = chL.clone(); chR.position.x = 0.14; bot.add(chL, chR); const antStem = new THREE.Mesh( new THREE.CylinderGeometry(0.02, 0.025, 0.16, 10), shellDark, ); antStem.position.set(0, 0.72, 0); bot.add(antStem); const antBall = new THREE.Mesh(new THREE.SphereGeometry(0.055, 16, 16), eyeGlow); antBall.position.set(0, 0.82, 0); bot.add(antBall); const armGeo = new THREE.CapsuleGeometry(0.07, 0.22, 6, 12); const armL = new THREE.Mesh(armGeo, shell); armL.position.set(-0.42, 0.08, 0); armL.rotation.z = 0.35; const armR = new THREE.Mesh(armGeo, shell); armR.position.set(0.42, 0.08, 0); armR.rotation.z = -0.35; bot.add(armL, armR); const leg = new THREE.Mesh(new THREE.CapsuleGeometry(0.09, 0.12, 6, 12), shellDark); leg.position.set(-0.12, -0.32, 0.05); const leg2 = leg.clone(); leg2.position.x = 0.12; bot.add(leg, leg2); root.add(bot); const orbMat = [ new THREE.MeshStandardMaterial({ color: 0xf0abfc, roughness: 0.35, metalness: 0.2 }), new THREE.MeshStandardMaterial({ color: 0x7dd3fc, roughness: 0.35, metalness: 0.2 }), new THREE.MeshStandardMaterial({ color: 0xfde68a, roughness: 0.35, metalness: 0.15 }), ]; const orbs: THREE.Mesh[] = []; for (let i = 0; i < 3; i++) { const o = new THREE.Mesh(new THREE.SphereGeometry(0.09, 16, 16), orbMat[i]); const a = (i / 3) * Math.PI * 2; o.position.set(Math.cos(a) * 1.15, 0.25 + i * 0.08, Math.sin(a) * 1.15); root.add(o); orbs.push(o); } return { bot, halo, orbs }; } 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: false, powerPreference: "high-performance", }); const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100); camera.position.set(0.35, 0.45, 3.85); const group = new THREE.Group(); scene.add(group); let anim: { dog?: THREE.Group; tail?: THREE.Mesh; bot?: THREE.Group; halo?: THREE.Mesh; orbs?: THREE.Mesh[]; } | null = null; if (variant === "indie") { scene.fog = new THREE.Fog(0xfff5f7, 3.8, 11); applyFilmicRenderer(renderer, 1.02); const hemi = new THREE.HemisphereLight(0xffffff, 0xfce7f3, 0.95); scene.add(hemi); const key = new THREE.DirectionalLight(0xffffff, 0.48); key.position.set(2, 5, 3); scene.add(key); const fill = new THREE.DirectionalLight(0xffe4e8, 0.28); fill.position.set(-3, 2, -1); scene.add(fill); const scr = new THREE.PointLight(0xbfdbfe, 0.35, 5); scr.position.set(-0.25, 0, 0); scene.add(scr); anim = buildIndieEnclosedRoom(group); group.rotation.set(0.1, 0.28, 0); } else { scene.fog = new THREE.FogExp2(0x0c0618, 0.048); applyFilmicRenderer(renderer, 1.05); const hemi = new THREE.HemisphereLight(0xc4b5fd, 0x1e1038, 0.75); scene.add(hemi); const key = new THREE.DirectionalLight(0xf5f3ff, 0.5); key.position.set(2, 4.5, 3); scene.add(key); const rim = new THREE.DirectionalLight(0x7c3aed, 0.22); rim.position.set(-2.5, 1, -1.5); scene.add(rim); const glow = new THREE.PointLight(0xa78bfa, 0.4, 9); glow.position.set(0.8, 1.2, 1); scene.add(glow); anim = buildAiCuteCyber(group); group.rotation.set(0.06, -0.15, 0); } let rx = group.rotation.x; let ry = group.rotation.y; let vx = 0; let vy = 0; const sens = 0.0046; const damping = 0.936; const autoRy = variant === "indie" ? 0.001 : 0.0013; 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(240, Math.round(w * 0.52))); 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 (anim?.dog) { anim.dog.position.y = -0.86 + Math.sin(t * 2) * 0.016; anim.dog.rotation.y = Math.sin(t * 0.85) * 0.05; } if (anim?.tail) { anim.tail.rotation.z = Math.PI / 2.8 + Math.sin(t * 4.5) * 0.22; } if (anim?.bot) { anim.bot.position.y = -0.25 + Math.sin(t * 1.5) * 0.03; anim.bot.rotation.y = Math.sin(t * 0.6) * 0.04; } if (anim?.halo) { anim.halo.rotation.z = t * 0.15; } if (anim?.orbs) { anim.orbs.forEach((o, i) => { const a = t * 0.5 + (i / 3) * Math.PI * 2; o.position.y = 0.25 + i * 0.08 + Math.sin(t * 1.8 + i) * 0.06; o.position.x = Math.cos(a) * 1.15; o.position.z = Math.sin(a) * 1.15; }); } 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" ? "独立开发互动预览" : "AI Agent 互动预览"; return (
); }