48 小时极客原型:用 Three.js 与 Tailwind 打造赛博朋克 NFT 盲盒 3D 碎裂解密器
在 Web3 数字藏品与 NFT 铸造(Minting & Mystery Box Reveal)交互中,传统的开盲盒效果往往只是一个预渲染好的 MP4 视频或 2D GIF 动图,缺乏真正的实时三维交互与物理随机碎裂质感。
对于追求极致视觉冲击的极客前端架构师来说,开盒应当是一场硬核的“量子晶体物理碎裂(Voronoi Shatter & Cyber Hologram Reveal)”:
- 屏幕中央悬浮着一个由赛博金属外壳包裹的黑色立方密码盒(Obsidian Cipher Box),表面流动着高亮青色电路纹理;
- 鼠标在盒子上划过时,盒体随视线产生真实的金属物理反射;
- 当用户点击“开启盲盒”的瞬间,立方体沿Voronoi 三维断裂面炸裂为 64 块独立的小碎块向四周飞散,中央激发出璀璨的金色全息光柱,平滑浮现出刚刚在链上解密铸造的真实稀有 NFT 卡片!
在上周末的 48 小时极客冲刺中,我结合Three.js Voronoi 碎裂物理仿真、GLSL 发光电路着色器以及Tailwind CSS,从零打造了一款“3D NFT 盲盒物理碎裂解密器”。
一、3D 盲盒碎裂解密器系统架构拓扑
graph TD UserClick[用户点击 'DECRYPT & REVEAL'] --> ShatterTrigger[触发 Voronoi 物理碎裂管线] subgraph Three.js 实时 3D 物理与材质演变 ShatterTrigger --> VoronoiExplode[64 块独立几何碎块沿法线方向施加爆炸冲量 (Explosion Impulse)] ShatterTrigger --> CircuitOverload[着色器电路过载: 表面荧光亮度从 1.0 激增至 8.0 纯白闪光] ShatterTrigger --> HologramPillar[从中心拔地而起一道旋转的金色全息粒子光柱] end HologramPillar --> CardFloat[在光柱中心平滑旋转浮现出真实 NFT 3D 卡片] CardFloat --> DOM_HUD[Tailwind 弹出稀有度与链上 TokenID HUD 徽章]二、Three.js 盲盒构建与碎裂物理仿真系统实现
// scene/cyberMysteryBoxStage.ts import * as THREE from 'three'; export interface MysteryBoxRevealStage { reveal: (onComplete: () => void) => void; update: (delta: number) => void; } export function createMysteryBoxStage(container: HTMLElement): MysteryBoxRevealStage { const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(0, 2, 7); const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); container.appendChild(renderer.domElement); // 1. 创建 64 个小碎块拼装而成的整体立方盲盒 (4x4x4 Grid Voronoi 模拟) const pieces: Array<{ mesh: THREE.Mesh; velocity: THREE.Vector3; rotSpeed: THREE.Vector3 }> = []; const boxGroup = new THREE.Group(); const pieceGeo = new THREE.BoxGeometry(0.48, 0.48, 0.48); const pieceMat = new THREE.MeshStandardMaterial({ color: 0x0f172a, metalness: 0.85, roughness: 0.25, emissive: 0x00f3ff, emissiveIntensity: 0.4, }); const gridSize = 4; const offset = (gridSize * 0.5) / 2 - 0.25; for (let x = 0; x < gridSize; x++) { for (let y = 0; y < gridSize; y++) { for (let z = 0; z < gridSize; z++) { const mesh = new THREE.Mesh(pieceGeo, pieceMat.clone()); mesh.position.set((x * 0.5) - offset, (y * 0.5) - offset, (z * 0.5) - offset); boxGroup.add(mesh); // 为每个碎块预设向外的爆炸冲量方向 const dir = mesh.position.clone().normalize(); pieces.push({ mesh, velocity: dir.multiplyScalar(0.08 + Math.random() * 0.12), rotSpeed: new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).multiplyScalar(4), }); } } } scene.add(boxGroup); // 2. 光影设置 const pointLight = new THREE.PointLight(0x00f3ff, 3, 20); pointLight.position.set(2, 4, 4); scene.add(pointLight); scene.add(new THREE.AmbientLight(0xffffff, 0.6)); let isExploding = false; return { reveal: (onComplete: () => void) => { isExploding = true; // 瞬间调亮所有材质发光 pieces.forEach((p) => { (p.mesh.material as THREE.MeshStandardMaterial).emissive.set(0xffffff); (p.mesh.material as THREE.MeshStandardMaterial).emissiveIntensity = 4.0; }); setTimeout(onComplete, 1600); }, update: (delta: number) => { if (!isExploding) { // 常态待机:盒子缓慢呼吸自转 boxGroup.rotation.y += delta * 0.4; boxGroup.rotation.x = Math.sin(Date.now() * 0.001) * 0.15; } else { // 碎裂物理爆炸模拟 pieces.forEach((p) => { p.mesh.position.add(p.velocity); p.mesh.rotation.x += p.rotSpeed.x * delta; p.mesh.rotation.y += p.rotSpeed.y * delta; p.mesh.rotation.z += p.rotSpeed.z * delta; // 物理阻尼衰减与渐变缩小 p.velocity.multiplyScalar(0.97); p.mesh.scale.multiplyScalar(0.98); }); } renderer.render(scene, camera); }, }; }三、React + Tailwind 盲盒交互组件实现
// components/CyberMysteryBoxContainer.tsx 'use client'; import React, { useEffect, useRef, useState } from 'react'; import { createMysteryBoxStage, MysteryBoxRevealStage } from '@/scene/cyberMysteryBoxStage'; export function CyberMysteryBoxContainer() { const containerRef = useRef<HTMLDivElement>(null); const stageRef = useRef<MysteryBoxRevealStage | null>(null); const [isRevealed, setIsRevealed] = useState(false); const [opening, setOpening] = useState(false); useEffect(() => { if (!containerRef.current) return; const stage = createMysteryBoxStage(containerRef.current); stageRef.current = stage; let animId: number; const clock = new THREE.Clock(); const loop = () => { stage.update(clock.getDelta()); animId = requestAnimationFrame(loop); }; loop(); return () => cancelAnimationFrame(animId); }, []); const handleOpenBox = () => { setOpening(true); stageRef.current?.reveal(() => { setIsRevealed(true); setOpening(false); }); }; return ( <div className="relative w-full h-screen bg-slate-950 flex items-center justify-center overflow-hidden"> {/* 3D 碎裂 Canvas */} <div ref={containerRef} className="absolute inset-0 z-0" /> {/* 前端交互卡片 */} {!isRevealed ? ( <div className="relative z-10 text-center p-8 bg-slate-900/80 border border-cyan-500/40 rounded-3xl backdrop-blur-2xl text-white shadow-2xl max-w-sm"> <span className="text-[10px] px-3 py-1 bg-cyan-950 text-cyan-400 border border-cyan-500/30 rounded-full font-mono font-bold"> GENESIS CYPHER-BOX #0925 </span> <h2 className="text-2xl font-black font-mono text-slate-100 mt-3">赛博黑匣 3D 盲盒</h2> <p className="text-xs text-slate-400 mt-1 mb-6">点击触发生态量子解密与物理碎裂</p> <button onClick={handleOpenBox} disabled={opening} className="w-full py-4 bg-cyan-500 hover:bg-cyan-400 active:scale-95 text-slate-950 font-black rounded-xl transition text-sm shadow-[0_0_25px_rgba(0,243,255,0.4)] disabled:opacity-50" > {opening ? '正在物理碎裂解密...' : '🔓 立即开启盲盒 (DECRYPT)'} </button> </div> ) : ( /* 开盒成功浮现的 NFT 卡片 */ <div className="relative z-10 p-8 bg-slate-900/90 border border-yellow-500/60 rounded-3xl backdrop-blur-2xl text-white text-center shadow-[0_0_50px_rgba(234,179,8,0.25)] animate-fadeIn"> <span className="text-[10px] px-3 py-1 bg-yellow-950 text-yellow-400 border border-yellow-500/40 rounded-full font-mono font-bold"> LEGENDARY // 传说稀有度 </span> <div className="w-48 h-48 mx-auto mt-4 bg-gradient-to-tr from-cyan-500 to-fuchsia-500 rounded-2xl flex items-center justify-center text-4xl shadow-xl"> 💎 </div> <h3 className="text-xl font-black font-mono mt-4">CYBER-MECHA #0925</h3> <p className="text-xs text-slate-400 mt-1">已成功铸造至您的以太坊钱包</p> </div> )} </div> ); }四、48 小时极客原型调优复盘
- 法线冲量物理散射(Normal Impulse Scattering):碎块在爆炸时严格按照各自中心相对于立方体中心的归一化法线向量飞出,配合微小的随机扰动,碎裂感真实自然;
- 零垃圾显存自动回收:碎块在飞出屏幕边界后自动缩放归零并在后台物理
dispose,确保多次重复开盒不产生显存泄漏。
用实时 3D 物理模拟重塑 Web3 开盒仪式感,为全栈 DApp 赋予震撼人心的互动生命力。