在VS Code中安装Live Server插件后创建文件index.html和main.js文件

(内容由Gemini 3生成)

HTML 部分:

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Three.js 粒子手势控制</title>
    <style>
        body {
            margin: 0;
            background-color: #000;
            color: #fff;
            font-family: sans-serif;
            overflow: hidden; 
        }
        #canvas-container {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
        }
        canvas {
            display: block;
        }
        /* 隐藏用于手势识别的视频元素 */
        #video-input {
            display: none;
        }
        /* 全屏按钮 */
        #fullscreen-btn {
            position: absolute;
            bottom: 20px;
            right: 20px;
            background: rgba(255, 255, 255, 0.2);
            border: 1px solid rgba(255, 255, 255, 0.3);
            color: white;
            padding: 10px 15px;
            cursor: pointer;
            z-index: 100;   
            border-radius: 5px;
            font-size: 14px;
        }
        .lil-gui {
            z-index: 101 !important;
        }
    </style>
</head>
<body>
    <video id="video-input"></video>
    <div id="canvas-container"></div>
    <button id="fullscreen-btn">全屏</button>
    
    <script src="https://cdn.jsdelivr.net/npm/three@0.149.0/build/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/lil-gui@0.19.1/dist/lil-gui.umd.min.js"></script>
    
    <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands@0.4.1675469240/hands.js" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils@0.1/camera_utils.js" crossorigin="anonymous"></script>
    <script src="main.js"></script>
</body>
</html>

JS部分:

// --- main.js 文件最顶部(在 window.addEventListener 之外)---
var modelData = {}; // 确保它是全局可访问的
// ...

window.addEventListener('load', () => {
    // ...
    // ... (所有其他代码)
    // ...
});
// --- main.js (最终稳定版) ---
window.addEventListener('load', () => {

    // --- 核心变量和状态 ---
    let scene, camera, renderer;
    let particles, particleMaterial;
    const particleCount = 20000;
    let modelData = {}; 
    
    // 状态和手动控制变量
    let controls = { 
        morphing: false,
        handOpenness: 0.0, 
        useManualControl: false 
    }; 

    // UI 设置
    const settings = {
        particleColor: '#00ffaa', 
        shape: 'heart',
    };
    
    // --- 调试:测试立方体 ---
    function initTestCube() {
        const geometry = new THREE.BoxGeometry(1, 1, 1);
        const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true });
        const cube = new THREE.Mesh(geometry, material);
        cube.position.set(0, 0, 0); 
        scene.add(cube);
    }
    
    // --- 粒子初始化 ---
    function initParticles() {
        const geometry = new THREE.BufferGeometry();
        const positions = new Float32Array(particleCount * 3).fill(0);
        const targets = new Float32Array(particleCount * 3).fill(0);
        const randoms = new Float32Array(particleCount * 3);

        for (let i = 0; i < particleCount; i++) {
            const i3 = i * 3;
            // 存储用于扩散的随机方向
            randoms[i3 + 0] = (Math.random() - 0.5) * 2.0;
            randoms[i3 + 1] = (Math.random() - 0.5) * 2.0;
            randoms[i3 + 2] = (Math.random() - 0.5) * 2.0;
        }

        geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
        geometry.setAttribute('aTarget', new THREE.BufferAttribute(targets, 3));
        geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 3));

        // 着色器材质
        particleMaterial = new THREE.ShaderMaterial({
            uniforms: {
                uHandOpenness: { value: 0.0 }, 
                uColor: { value: new THREE.Color(settings.particleColor) },
                uMorphProgress: { value: 0.0 }
            },
            vertexShader: `
                attribute vec3 aTarget;
                attribute vec3 aRandom;
                uniform float uHandOpenness;
                uniform float uMorphProgress;

                void main() {
                    vec3 morphedPosition = mix(position, aTarget, uMorphProgress);

                    float diffusion = uHandOpenness * 10.0;
                    vec3 finalPosition = morphedPosition + (aRandom * diffusion);

                    vec4 modelViewPosition = modelViewMatrix * vec4(finalPosition, 1.0);
                    gl_Position = projectionMatrix * modelViewPosition;
                    
                    gl_PointSize = 3.0; 
                }
            `,
            fragmentShader: `
                uniform vec3 uColor;
                void main() {
                    float f = length(gl_PointCoord - vec2(0.5, 0.5));
                    if (f > 0.5) discard; 
                    gl_FragColor = vec4(uColor, 1.0);
                }
            `,
            blending: THREE.AdditiveBlending,
            depthWrite: false,
            transparent: true
        });

        particles = new THREE.Points(geometry, particleMaterial);
        scene.add(particles);
    }

    // --- 模型数据生成 ---
    // --- 替换 main.js 中的 generateModelData 函数 ---
function generateModelData() {
    // --- 1. 爱心 (heart): 保持不变 ---
    modelData.heart = new Float32Array(particleCount * 3);
    const scale = 5;
    for (let i = 0; i < particleCount; i++) {
        const i3 = i * 3;
        const t = Math.random() * Math.PI * 2;
        const x = scale * 1.6 * Math.pow(Math.sin(t), 3);
        const y = scale * (1.3 * Math.cos(t) - 0.5 * Math.cos(2*t) - 0.2 * Math.cos(3*t) - 0.1 * Math.cos(4*t));
        const z = (Math.random() - 0.5) * 0.5;
        modelData.heart[i3 + 0] = x;
        modelData.heart[i3 + 1] = y;
        modelData.heart[i3 + 2] = z;
    }
    
    // --- 2. 花朵 (flower): 新增数据生成逻辑 ---
    modelData.flower = new Float32Array(particleCount * 3);
    const flowerScale = 6;
    const petalCount = 8; // 花瓣数量
    
    for (let i = 0; i < particleCount; i++) {
        const i3 = i * 3;
        
        // 使用球坐标生成点
        const r = Math.random() * 0.7 + 0.3; // 半径
        const theta = Math.random() * Math.PI * 2; // 角度 (0到360度)
        const phi = Math.random() * Math.PI; // 角度 (0到180度)

        // 生成一个标准球体上的点
        let x = r * Math.sin(phi) * Math.cos(theta);
        let y = r * Math.sin(phi) * Math.sin(theta);
        let z = r * Math.cos(phi);

        // 引入花瓣形状的扭曲 (根据theta角度周期性变化)
        const petalFactor = (1.0 + Math.cos(theta * petalCount)) * 0.5; 
        
        // 在径向上扭曲点,形成花瓣
        x *= (petalFactor * 0.6 + 0.4);
        y *= (petalFactor * 0.6 + 0.4);

        // 赋予花朵模型数据
        modelData.flower[i3 + 0] = x * flowerScale;
        modelData.flower[i3 + 1] = y * flowerScale;
        modelData.flower[i3 + 2] = z * flowerScale * 0.5; // 压扁一点
    }

    // --- 3. 其他占位模型 (saturn, buddha) ---
    modelData.saturn = new Float32Array(particleCount * 3).fill(0); 
    modelData.buddha = new Float32Array(particleCount * 3).fill(0);
}

    // --- 模型切换函数 ---
    function switchModel(modelName, immediate = false) {
        if (modelName === 'fireworks') {
             console.warn("此版本未实现烟花物理着色器。");
             return;
        }

        const newTargets = modelData[modelName] || new Float32Array(particleCount * 3).fill(0); 
        
        const geom = particles.geometry;
        
        // 将当前的目标 (aTarget) 复制到起始位置 (position)
        geom.attributes.position.copy(geom.attributes.aTarget);
        geom.attributes.position.needsUpdate = true;

        // 设置新的目标位置
        geom.setAttribute('aTarget', new THREE.BufferAttribute(newTargets, 3));
        geom.attributes.aTarget.needsUpdate = true;

        if (immediate) {
            particleMaterial.uniforms.uMorphProgress.value = 1.0;
            controls.morphing = false;
        } else {
            particleMaterial.uniforms.uMorphProgress.value = 0.0;
            controls.morphing = true;
        }
    }

    // --- MediaPipe 结果回调 ---
    function onHandResults(results) {
        if (controls.useManualControl) return; 

        if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
            const landmarks = results.multiHandLandmarks[0];
            const wrist = landmarks[0];
            const fingerTips = [landmarks[4], landmarks[8], landmarks[12], landmarks[16], landmarks[20]];

            let totalDistance = 0;
            for (const tip of fingerTips) {
                const dx = tip.x - wrist.x;
                const dy = tip.y - wrist.y;
                totalDistance += Math.sqrt(dx*dx + dy*dy);
            }
            const avgDistance = totalDistance / fingerTips.length;

            const minOpen = 0.15; 
            const maxOpen = 0.4;  
            
            let openness = (avgDistance - minOpen) / (maxOpen - minOpen);
            controls.handOpenness = Math.max(0.0, Math.min(1.0, openness)); 

        } else {
            // 没有检测到手,缓慢归零
            controls.handOpenness = controls.handOpenness * 0.95;
        }
    }

    // --- 初始化 MediaPipe (强化错误捕获) ---
    function initMediaPipe() {
        try {
            const videoElement = document.getElementById('video-input');

            if (typeof Hands === 'undefined' || typeof Camera === 'undefined') {
                console.error("MediaPipe 库 (Hands/Camera) 未加载,请检查 index.html 中的 CDN 链接。");
                alert("MediaPipe 库加载失败!请尝试使用手动控制。");
                controls.useManualControl = true;
                return;
            }

            const hands = new Hands({
                // 使用官方推荐的 locateFile 路径
                locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands@0.4.1675469240/${file}`
            });

            hands.setOptions({ maxNumHands: 1, modelComplexity: 1, minDetectionConfidence: 0.5 });
            hands.onResults(onHandResults);

            const mpCamera = new Camera(videoElement, {
                onFrame: async () => { await hands.send({ image: videoElement }); },
                width: 640, height: 480
            });
            mpCamera.start();
            console.log("MediaPipe 尝试启动摄像头成功。");

        } catch (e) {
            console.error("MediaPipe 启动时发生致命错误,可能原因:摄像头权限拒绝或 CDN 链接错误。", e);
            alert("摄像头启动失败!已自动切换到手动控制模式。");
            controls.useManualControl = true;
        }
    }

    // --- UI 控制 ---
    function initGUI() {
        const gui = new lil.GUI();
        
        gui.addColor(settings, 'particleColor')
           .name('粒子颜色')
           .onChange((value) => { particleMaterial.uniforms.uColor.value.set(value); });

        // 修复 ReferenceError: switchModel is not defined
        gui.add(settings, 'shape', ['heart', 'saturn', 'buddha', 'flower', 'fireworks'])
           .name('选择模型')
           .onChange(switchModel); // 直接引用函数名
        
        const handFolder = gui.addFolder('手势控制 (调试)');
        
        handFolder.add(controls, 'useManualControl')
           .name('手动模式 (开/关)');
        
        handFolder.add(controls, 'handOpenness', 0.0, 1.0, 0.01)
            .name('手动张合度')
            .step(0.01)
            .listen(); // 监听值,即使摄像头在运行时也能看到变化
        
        handFolder.open();
    }

    // --- 窗口大小调整 ---
    function onWindowResize() {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
    }
    
    // --- 全屏控制 ---
    function initFullscreen() {
        const fullscreenBtn = document.getElementById('fullscreen-btn');
        fullscreenBtn.addEventListener('click', () => {
            if (!document.fullscreenElement) {
                document.documentElement.requestFullscreen();
            } else {
                document.exitFullscreen();
            }
        });
    }

    // --- 场景主初始化函数 ---
    function init() {
        scene = new THREE.Scene();
        
        camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.z = 15;
        
        const container = document.getElementById('canvas-container');
        renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(window.devicePixelRatio);
        container.appendChild(renderer.domElement);

        // 初始化顺序:先几何,后工具
        initTestCube(); 
        initParticles();
        generateModelData();

        initGUI(); 
        initFullscreen(); // 修复 ReferenceError: initFullscreen is not defined
        initMediaPipe();

        switchModel(settings.shape, true); 

        window.addEventListener('resize', onWindowResize);
        animate();
    }

    // --- 动画循环 ---
    function animate() {
        requestAnimationFrame(animate);

        // 确定用于扩散的最终值
        const particleDiffusionValue = controls.useManualControl 
            ? controls.handOpenness 
            : controls.handOpenness; 
        
        // 实时平滑更新 uniform
        const currentOpenness = particleMaterial.uniforms.uHandOpenness.value;
        particleMaterial.uniforms.uHandOpenness.value += 
            (particleDiffusionValue - currentOpenness) * 0.15; 
        
        // 旋转
        const testCube = scene.children.find(child => child.isMesh);
        if(testCube) { testCube.rotation.y += 0.01; }
        particles.rotation.y += 0.0005;

        renderer.render(scene, camera);
    }

    // --- 启动! ---
    init();
});

 

 

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐