最近网上很火的利用Gemini 3 Pro 手势控制粒子视频

AI ·  3个月前 · 249人浏览

使用说明(文档最下方附个人演示视频)<使用此文档源码可达到视频效果,如需个人定制需组织提示词不断喂给AI校正优化>

  1. 保存代码:将下方的代码保存为 index.html

  2. 运行环境:由于涉及摄像头权限,浏览器通常要求在 HTTPSLocalhost 环境下运行。

    • 如果你有 VS Code,安装 "Live Server" 插件,右键 index.html -> "Open with Live Server"。

    • 或者使用 Python: python -m http.server

  3. 授权:打开页面后允许摄像头权限。

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebAR 圣诞树 - 交互修复版</title>
    <style>
        body { margin: 0; overflow: hidden; background-color: #000; font-family: 'Consolas', monospace; }
        #canvas-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
        #video-input { display: none; } /* 隐藏原始视频 */

        /* 调试面板 */
        #debug-panel {
            position: absolute; top: 10px; left: 10px; z-index: 20;
            background: rgba(0, 20, 0, 0.8); border: 1px solid #00ff00;
            color: #00ff00; padding: 10px; font-size: 12px;
            pointer-events: none; width: 220px;
        }
        .bar-container { display: flex; align-items: center; margin-bottom: 4px; }
        .bar-label { width: 60px; }
        .bar-value { color: #fff; font-weight: bold; }

        /* 引导提示 */
        #instruction {
            position: absolute; bottom: 20px; width: 100%; text-align: center;
            color: rgba(255, 255, 255, 0.5); font-size: 14px; z-index: 10;
            pointer-events: none; text-shadow: 0 0 5px #000;
        }

        #loading {
            position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
            color: #d4af37; font-size: 20px; z-index: 30; text-shadow: 0 0 10px #d4af37;
        }
    </style>

    <script src="https://unpkg.com/three@0.128.0/build/three.min.js"></script>
    <script src="https://unpkg.com/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
    <script src="https://unpkg.com/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script>
    <script src="https://unpkg.com/@mediapipe/hands/hands.js" crossorigin="anonymous"></script>
</head>
<body>

<div id="debug-panel">
    <div style="border-bottom:1px solid #00ff00; margin-bottom:5px; padding-bottom:5px;">系统状态监视器</div>
    <div class="bar-container"><span class="bar-label">手部检测:</span> <span id="debug-detected" class="bar-value">NO</span></div>
    <div class="bar-container"><span class="bar-label">当前手势:</span> <span id="debug-gesture" class="bar-value" style="color:yellow">等待...</span></div>
    <div class="bar-container"><span class="bar-label">控制旋转:</span> <span id="debug-rotate" class="bar-value">0.0</span></div>
    <hr style="border-color: #004400;">
    <div>手指状态 (1=直, 0=弯):</div>
    <div id="debug-fingers">👍[?] ☝️[?] 🖕[?] 💍[?] 🤙[?]</div>
</div>

<div id="loading">正在装载 25,000 个光子...<br>请授予摄像头权限</div>
<div id="instruction">✊握拳:凝聚圣诞树 | 🖐张开:散开星云 | ↔️左右移动手:旋转场景</div>

<video id="video-input"></video>
<div id="canvas-container"></div>

<script type="x-shader/x-vertex" id="vertexshader">
    attribute float size;
    attribute vec3 customColor;
    attribute float blinkOffset; // 闪烁偏移量

    varying vec3 vColor;
    varying float vBlink;

    uniform float uTime;
    uniform float uPixelRatio;

    void main() {
        vColor = customColor;
        // 计算闪烁因子:基于时间的 Sin 波
        vBlink = 0.8 + 0.4 * sin(uTime * 3.0 + blinkOffset);

        vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
        // 距离衰减公式:离得越远粒子越小
        gl_PointSize = size * uPixelRatio * ( 200.0 / -mvPosition.z );
        gl_Position = projectionMatrix * mvPosition;
    }
</script>

<script type="x-shader/x-fragment" id="fragmentshader">
    varying vec3 vColor;
    varying float vBlink;

    void main() {
        // 创建圆形柔化粒子
        vec2 xy = gl_PointCoord.xy - vec2(0.5);
        float dist = length(xy);

        if ( dist > 0.5 ) discard;

        // 核心高亮,边缘柔和
        float glow = 1.0 - smoothstep(0.1, 0.5, dist);

        // 混合颜色、发光强度和闪烁
        gl_FragColor = vec4( vColor * 1.5, glow * vBlink ); 
    }
</script>

<script>
    // --- 参数配置 ---
    const CONFIG = {
        count: 25000,
        colors: {
            leaf: [0x0f2e18, 0x1b4020, 0x2f5e36], // 深浅不一的绿色
            ornament: [0xff0000, 0xd4af37, 0xffffff, 0x00ccff] // 红金白蓝装饰
        },
        camZ: 60
    };

    let scene, camera, renderer, material;
    let particles, positions, targetPositions, currentPositions;
    let time = 0;

    // 交互状态
    let interaction = {
        hasHand: false,
        gesture: 'cloud', // 'tree' or 'cloud'
        rotSpeed: 0
    };

    init();
    initMediaPipe();

    function init() {
        // 1. 场景
        scene = new THREE.Scene();
        scene.fog = new THREE.FogExp2(0x000500, 0.01); // 墨绿色深邃背景

        camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 1000);
        camera.position.z = CONFIG.camZ;
        camera.position.y = 10;
        camera.lookAt(0, 10, 0);

        // 2. 渲染器
        renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.getElementById('canvas-container').appendChild(renderer.domElement);

        // 3. 粒子系统初始化
        initParticles();

        // 4. 事件
        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        });

        animate();
    }

    function initParticles() {
        const geometry = new THREE.BufferGeometry();
        positions = new Float32Array(CONFIG.count * 3);
        const colors = new Float32Array(CONFIG.count * 3);
        const sizes = new Float32Array(CONFIG.count);
        const blinkOffsets = new Float32Array(CONFIG.count);

        currentPositions = [];
        targetPositions = [];

        for (let i = 0; i < CONFIG.count; i++) {
            // 初始形态:散乱的云
            const x = (Math.random() - 0.5) * 100;
            const y = (Math.random() - 0.5) * 60 + 10;
            const z = (Math.random() - 0.5) * 80;

            positions[i*3] = x;
            positions[i*3+1] = y;
            positions[i*3+2] = z;

            currentPositions.push(new THREE.Vector3(x, y, z));
            targetPositions.push(new THREE.Vector3(x, y, z)); // 初始目标也是云

            // 颜色分配逻辑:80%是树叶,20%是彩灯
            const isOrnament = Math.random() > 0.85;
            const colorHex = isOrnament 
                ? CONFIG.colors.ornament[Math.floor(Math.random() * CONFIG.colors.ornament.length)]
                : CONFIG.colors.leaf[Math.floor(Math.random() * CONFIG.colors.leaf.length)];

            const color = new THREE.Color(colorHex);
            colors[i*3] = color.r;
            colors[i*3+1] = color.g;
            colors[i*3+2] = color.b;

            // 尺寸:彩灯大一点,树叶小一点
            sizes[i] = isOrnament ? (1.5 + Math.random()) : (0.6 + Math.random() * 0.4);
            blinkOffsets[i] = Math.random() * 10;
        }

        geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
        geometry.setAttribute('customColor', new THREE.BufferAttribute(colors, 3));
        geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
        geometry.setAttribute('blinkOffset', new THREE.BufferAttribute(blinkOffsets, 1));

        material = new THREE.ShaderMaterial({
            uniforms: {
                uTime: { value: 0 },
                uPixelRatio: { value: window.devicePixelRatio }
            },
            vertexShader: document.getElementById('vertexshader').textContent,
            fragmentShader: document.getElementById('fragmentshader').textContent,
            transparent: true,
            depthWrite: false,
            blending: THREE.AdditiveBlending
        });

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

        calculateShape('cloud'); // 默认形态
    }

    // --- 核心算法:生成逼真的分层圣诞树 ---
    function calculateShape(type) {
        if (type === 'tree') {
            const layers = 18; // 树的层数
            const treeHeight = 55;
            const baseRadius = 25;

            for (let i = 0; i < CONFIG.count; i++) {
                // 将粒子分配到各个层级
                // 使用 pow 使得上方层级粒子少,下方多
                const layerIndex = Math.floor(Math.pow(Math.random(), 0.8) * layers);
                const layerRatio = layerIndex / layers; // 0(顶) -> 1(底)

                // 每层的中心高度
                const layerY = (1 - layerRatio) * treeHeight - 15;

                // 该层的半径 (线性扩大)
                const layerMaxR = layerRatio * baseRadius;

                // 在层内分布:倾向于外圈(树枝末端)但也有内部填充
                const rRatio = Math.sqrt(Math.random()); 
                const r = rRatio * layerMaxR;
                const theta = Math.random() * Math.PI * 2;

                // 核心:枝叶下垂效果 (Gravity Droop)
                // 离中心越远,下垂越厉害
                const droop = rRatio * rRatio * 4.0; 

                targetPositions[i].set(
                    r * Math.cos(theta),
                    layerY - droop, // 高度受下垂影响
                    r * Math.sin(theta)
                );
            }
        } else {
            // 散开的星云
            for (let i = 0; i < CONFIG.count; i++) {
                const angle = Math.random() * Math.PI * 2;
                const r = 20 + Math.random() * 40;
                const y = (Math.random() - 0.5) * 50 + 10;
                targetPositions[i].set(
                    Math.cos(angle) * r,
                    y,
                    Math.sin(angle) * r
                );
            }
        }
    }

    // --- 动画循环 ---
    function animate() {
        requestAnimationFrame(animate);
        time += 0.015;
        material.uniforms.uTime.value = time;

        // 1. 场景旋转控制
        if (interaction.hasHand) {
            scene.rotation.y += interaction.rotSpeed;
        } else {
            scene.rotation.y += 0.002; // 无人时自动慢转
        }

        // 2. 粒子物理 (平滑插值)
        const positions = particles.geometry.attributes.position.array;

        for (let i = 0; i < CONFIG.count; i++) {
            const current = currentPositions[i];
            const target = targetPositions[i];

            // 简单的 Ease-out 插值,不用复杂的弹簧以保证性能和形状稳定性
            current.x += (target.x - current.x) * 0.06;
            current.y += (target.y - current.y) * 0.06;
            current.z += (target.z - current.z) * 0.06;

            positions[i*3] = current.x;
            positions[i*3+1] = current.y;
            positions[i*3+2] = current.z;
        }

        particles.geometry.attributes.position.needsUpdate = true;
        renderer.render(scene, camera);
    }

    // --- MediaPipe 逻辑 (修复手势判定) ---
    function initMediaPipe() {
        const videoElement = document.getElementById('video-input');
        const hands = new Hands({locateFile: (file) => `https://unpkg.com/@mediapipe/hands/${file}`});

        hands.setOptions({
            maxNumHands: 1,
            modelComplexity: 1,
            minDetectionConfidence: 0.6, // 稍微降低以提高检出率
            minTrackingConfidence: 0.6
        });

        hands.onResults(onHandsResults);

        const cameraUtils = new Camera(videoElement, {
            onFrame: async () => { await hands.send({image: videoElement}); },
            width: 640, height: 480
        });
        cameraUtils.start();
    }

    function onHandsResults(results) {
        document.getElementById('loading').style.display = 'none';

        const debugDetected = document.getElementById('debug-detected');
        const debugGesture = document.getElementById('debug-gesture');
        const debugRotate = document.getElementById('debug-rotate');
        const debugFingers = document.getElementById('debug-fingers');

        if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
            interaction.hasHand = true;
            debugDetected.innerText = "YES";
            debugDetected.style.color = "#00ff00";

            const lm = results.multiHandLandmarks[0];
            const palmX = (lm[0].x + lm[9].x) / 2; // 手掌中心 X 坐标

            // --- 修复1: 移动逻辑 ---
            // 0 (左) -- 0.5 (中) -- 1.0 (右)
            // 映射到旋转速度:左边转,右边转,中间停
            const deadZone = 0.15; // 中心死区
            let rot = 0;
            if (palmX < 0.5 - deadZone) rot = -0.02 * (0.5 - deadZone - palmX) * 10;
            if (palmX > 0.5 + deadZone) rot = 0.02 * (palmX - 0.5 - deadZone) * 10;
            interaction.rotSpeed = rot;
            debugRotate.innerText = rot.toFixed(3);

            // --- 修复2: 手势判定逻辑 (基于指尖与指关节距离) ---
            const wrist = lm[0];

            // 判断手指是否伸直:比较 指尖(Tip)到手腕距离 vs 指关节(PIP)到手腕距离
            // 如果 Tip 远大于 PIP,说明伸直了。如果差不多或更小,说明弯曲了。
            function isStraight(tipIdx, pipIdx) {
                const dTip = Math.hypot(lm[tipIdx].x - wrist.x, lm[tipIdx].y - wrist.y);
                const dPip = Math.hypot(lm[pipIdx].x - wrist.x, lm[pipIdx].y - wrist.y);
                return dTip > dPip * 1.2; // 1.2 系数容错
            }

            const f_thumb = isStraight(4, 2);
            const f_index = isStraight(8, 6);
            const f_middle = isStraight(12, 10);
            const f_ring = isStraight(16, 14);
            const f_pinky = isStraight(20, 18);

            // 更新 UI 状态
            const statusStr = (f) => f ? "1" : "0";
            debugFingers.innerText = `👍[${statusStr(f_thumb)}] ☝️[${statusStr(f_index)}] 🖕[${statusStr(f_middle)}] 💍[${statusStr(f_ring)}] 🤙[${statusStr(f_pinky)}]`;

            // 计算伸直的手指数量 (不含大拇指,因为大拇指有时很难判断)
            const extendedCount = (f_index?1:0) + (f_middle?1:0) + (f_ring?1:0) + (f_pinky?1:0);

            // --- 状态机切换 ---
            if (extendedCount <= 1) { 
                // 0或1根手指伸直 -> 判定为握拳 (容错率高)
                if (interaction.gesture !== 'tree') {
                    interaction.gesture = 'tree';
                    calculateShape('tree');
                    debugGesture.innerText = "握拳 (凝聚)";
                    debugGesture.style.color = "#00ffff";
                }
            } else if (extendedCount >= 3) {
                // 3根以上手指伸直 -> 判定为张开
                if (interaction.gesture !== 'cloud') {
                    interaction.gesture = 'cloud';
                    calculateShape('cloud');
                    debugGesture.innerText = "张开 (散开)";
                    debugGesture.style.color = "#ff00ff";
                }
            }

        } else {
            interaction.hasHand = false;
            debugDetected.innerText = "NO";
            debugDetected.style.color = "red";
            debugGesture.innerText = "---";
            interaction.rotSpeed = 0;
        }
    }
</script>
</body>
</html>

 

评论
2026 俞事-不知名人类的boke All Rights Reserved.
系统状态: 在线 | 网络延迟: 7ms
© 2025 JINTANG.PRO · POWERED BY JINTANG
见山方知山之高,临水才知水之渊