Three.js3D粒子实时交互(附提示词和代码)——Gemini 3
本文最后更新于174 天前,其中的信息可能已经过时,如有错误请发送邮件到big_fw@foxmail.com

这两天刷抖音,你一定见过那个火爆的—— 只需要一个摄像头,单手张合,就能让 3D 粒子实时缩放、扩散、变形……甚至可以变成爱心、土星、烟花等超炫模型。

很多博主说是 Gemini 3 的效果,其实背后真正的核心是 Three.js + MediaPipe 手势识别,Gemini 3 只是生成代码的“加速器”。

既然这么火,那咱们当然也要玩一把,而且要做得更完整、更可控、更适合集成到你自己的项目里。

🚀 使用网站(全部满速、无需魔法)

你可以直接用下面任一站点生成整个项目的代码:

https://share.zhangsan.cool
https://share-hk.zhangsan.cool
https://share.searchknowledge.cloud
https://hello.aiforme.cloud

全部支持 GPT-5.1 / Gemini 3 / Grok 4.1 / Banana 2 等多模型 AI 国内访问稳定、速度快、非常适合开发者。

🧠 只需一段提示词,生成完整 3D 交互系统

你可以把下面这段提示词原封不动丢给上述网站的Gemini-3-pro模型:

要求: 用Three.js创建一个实时交互的3D粒子系统。

1. 通过摄像头检测单手张合控制粒子群的缩放与扩散(单手张开时粒子扩散,单手合拢时粒子收缩,张合幅度与缩放/扩散程度成正比,幅度足够大);

2. 提供UI面板可选择爱心/花朵/土星/佛像/烟花等模型(粒子自动组成选中的模型轮廓,切换模型时粒子平滑过渡);

3. 支持颜色选择器调整粒子颜色(可选择单色,支持实时预览颜色变化);

4. 粒子需实时响应手势变化(延迟≤100ms,无卡顿)。

5. 界面简洁现代,包含全屏控制按钮(UI面板固定在页面右侧,不遮挡3D粒子区域,按钮样式统一、扁平化);

6. 无需额外依赖后端,所有功能前端实现,生成完整HTML文件,可直接在浏览器打开预览。

任何主流大模型都能按要求生成完整项目,但 Gemini 3 在 Web 交互代码生成方面尤为强劲,推荐使用。

🧩 生成的完整代码(可直接运行)

下面是 AI 生成的完整 HTML 文件,只需复制到 index.html,即可直接浏览器打开运行:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D手势粒子交互系统</title>
    <style>
        body { margin: 0; overflow: hidden; background-color: #050505; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
        canvas { display: block; }

        /* 隐藏的视频元素,用于MediaPipe处理 */
        #input_video { position: absolute; top: -1000px; left: -1000px; transform: scaleX(-1); }

        /* UI 面板 */
        #ui-container {
            position: absolute;
            top: 20px;
            right: 20px;
            width: 260px;
            background: rgba(30, 30, 30, 0.85);
            backdrop-filter: blur(10px);
            padding: 20px;
            border-radius: 12px;
            border: 1px solid rgba(255, 255, 255, 0.1);
            color: #fff;
            box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
            transition: all 0.3s ease;
            z-index: 10;
        }

        h2 { margin: 0 0 15px 0; font-size: 18px; font-weight: 500; letter-spacing: 1px; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 10px; }

        .control-group { margin-bottom: 20px; }
        label { display: block; margin-bottom: 8px; font-size: 12px; color: #aaa; text-transform: uppercase; }

        /* 按钮样式 */
        .shape-btn {
            display: inline-block;
            width: 48%;
            padding: 8px 0;
            margin: 0 1% 8px 0;
            background: rgba(255, 255, 255, 0.1);
            border: 1px solid rgba(255, 255, 255, 0.05);
            color: #ddd;
            border-radius: 6px;
            cursor: pointer;
            font-size: 13px;
            transition: all 0.2s;
            text-align: center;
        }
        .shape-btn:hover { background: rgba(255, 255, 255, 0.2); }
        .shape-btn.active { background: #00d2ff; color: #000; font-weight: bold; border-color: #00d2ff; }

        /* 颜色选择器 */
        input[type="color"] {
            -webkit-appearance: none;
            border: none;
            width: 100%;
            height: 35px;
            border-radius: 6px;
            cursor: pointer;
            background: none;
        }
        input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
        input[type="color"]::-webkit-color-swatch { border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; }

        /* 全屏按钮 */
        #fullscreen-btn {
            width: 100%;
            padding: 10px;
            background: transparent;
            border: 1px solid rgba(255, 255, 255, 0.2);
            color: #fff;
            border-radius: 6px;
            cursor: pointer;
            margin-top: 10px;
            transition: 0.2s;
        }
        #fullscreen-btn:hover { background: rgba(255, 255, 255, 0.1); }

        /* 状态指示器 */
        #status {
            position: absolute;
            top: 20px;
            left: 20px;
            color: #00d2ff;
            font-size: 14px;
            background: rgba(0,0,0,0.6);
            padding: 8px 15px;
            border-radius: 20px;
            pointer-events: none;
        }

        /* 摄像头预览小窗 (可选,便于调试) */
        #webcam-preview {
            position: absolute;
            bottom: 20px;
            left: 20px;
            width: 160px;
            height: 120px;
            background: #000;
            border-radius: 8px;
            overflow: hidden;
            border: 2px solid rgba(255,255,255,0.1);
            opacity: 0.7;
            transform: scaleX(-1); /* 镜像 */
        }
    </style>

    <!-- 引入 Three.js -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <!-- 引入 MediaPipe Hands -->
    <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script>
</head>
<body>

    <div id="status">正在初始化摄像头与模型...</div>
    <video id="input_video"></video>
    <canvas id="webcam-preview"></canvas>

    <div id="ui-container">
        <h2>粒子控制台</h2>

        <div class="control-group">
            <label>选择模型</label>
            <div id="shape-buttons">
                <div class="shape-btn active" data-shape="heart">爱心</div>
                <div class="shape-btn" data-shape="flower">花朵</div>
                <div class="shape-btn" data-shape="saturn">土星</div>
                <div class="shape-btn" data-shape="buddha">佛像(禅)</div>
                <div class="shape-btn" data-shape="firework">烟花球</div>
            </div>
        </div>

        <div class="control-group">
            <label>粒子颜色</label>
            <input type="color" id="color-picker" value="#00d2ff">
        </div>

        <button id="fullscreen-btn">⛶ 全屏模式</button>

        <div class="control-group" style="margin-top:20px; border-top: 1px solid rgba(255,255,255,0.1); padding-top:10px;">
            <label>手势数据</label>
            <div style="font-size:12px; color:#ddd; display:flex; justify-content:space-between;">
                <span>状态: <span id="hand-state" style="color:#aaa;">未检测</span></span>
                <span>系数: <span id="scale-val" style="color:#00d2ff;">1.0</span></span>
            </div>
        </div>
    </div>

<script>
    // ================= 配置参数 =================
    const PARTICLE_COUNT = 25000;
    const PARTICLE_SIZE = 0.08;
    const TRANSITION_SPEED = 0.08; // 形状变换平滑度
    const HAND_SENSITIVITY = 0.15; // 手势响应平滑度

    // ================= 全局变量 =================
    let scene, camera, renderer, particles, geometry, material;
    let currentShape = 'heart';
    let targetPositions = []; // 存储目标形状的坐标
    let basePositions = [];   // 存储当前计算出的基础形状坐标(无缩放)
    let currentColor = new THREE.Color(0x00d2ff);

    // 手势控制变量
    let handScaleFactor = 1.0;  // 当前应用的缩放因子
    let targetHandScale = 1.0;  // 目标缩放因子(来自MediaPipe)
    let isHandDetected = false;

    // UI 元素
    const statusEl = document.getElementById('status');
    const handStateEl = document.getElementById('hand-state');
    const scaleValEl = document.getElementById('scale-val');
    const previewCanvas = document.getElementById('webcam-preview');
    const previewCtx = previewCanvas.getContext('2d');

    // ================= Three.js 初始化 =================
    function initThree() {
        scene = new THREE.Scene();
        // 添加一点雾效果增加深度感
        scene.fog = new THREE.FogExp2(0x050505, 0.02);

        camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.z = 25;
        camera.position.y = 2;

        renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(window.devicePixelRatio);
        document.body.appendChild(renderer.domElement);

        // 创建粒子系统
        geometry = new THREE.BufferGeometry();
        const positions = new Float32Array(PARTICLE_COUNT * 3);

        // 初始位置随机分布
        for (let i = 0; i < PARTICLE_COUNT * 3; i++) {
            positions[i] = (Math.random() - 0.5) * 50;
        }

        geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));

        material = new THREE.PointsMaterial({
            color: currentColor,
            size: PARTICLE_SIZE,
            sizeAttenuation: true,
            blending: THREE.AdditiveBlending,
            depthWrite: false,
            transparent: true,
            opacity: 0.8
        });

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

        // 初始化目标位置数组
        targetPositions = new Float32Array(PARTICLE_COUNT * 3);
        basePositions = new Float32Array(PARTICLE_COUNT * 3);

        // 默认生成爱心
        generateShape('heart');

        // 监听窗口大小变化
        window.addEventListener('resize', onWindowResize, false);
    }

    function onWindowResize() {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
    }

    // ================= 形状生成算法 =================
    // 使用数学公式生成不同形状的点云
    function getPointOnShape(type, idx, total) {
        const u = Math.random();
        const v = Math.random();
        const theta = Math.random() * Math.PI * 2;
        const phi = Math.acos(2 * Math.random() - 1);

        let x, y, z;

        switch (type) {
            case 'heart':
                // 3D Heart equation
                // \[ x = 16\sin^3(t) \]
                // \[ y = 13\cos(t) - 5\cos(2t) - 2\cos(3t) - \cos(4t) \]
                // 扩展到3D: 结合层状分布或旋转体积
                {
                    const t = theta; // 0 to 2PI
                    // 稍微随机化层厚度
                    const r = 0.5 + Math.random() * 0.5;
                    // 爱心参数方程
                    x = 16 * Math.pow(Math.sin(t), 3);
                    y = 13 * Math.cos(t) - 5 * Math.cos(2*t) - 2 * Math.cos(3*t) - Math.cos(4*t);
                    z = (Math.random() - 0.5) * 8; // 厚度

                    // 让粒子填充内部
                    const scale = Math.cbrt(Math.random());
                    x *= scale; y *= scale; z *= scale;

                    // 调整比例
                    x *= 0.5; y *= 0.5; z *= 0.5;
                }
                break;

            case 'flower':
                // 3D Rose / Flower shape
                {
                    const k = 3; // 花瓣数
                    const r = 10 * Math.cos(k * theta) + 5; // 极坐标半径
                    const r_vol = r * Math.random(); // 填充内部

                    x = r_vol * Math.cos(theta);
                    y = r_vol * Math.sin(theta);
                    z = (Math.random() - 0.5) * 5 * (1 - r_vol/15); // 中心厚,边缘薄

                    // 稍微弯曲花瓣
                    z += Math.cos(theta * k) * 2;
                }
                break;

            case 'saturn':
                // Sphere + Ring
                if (Math.random() > 0.4) {
                    // Planet Body (Sphere)
                    const r = 6 * Math.cbrt(Math.random());
                    x = r * Math.sin(phi) * Math.cos(theta);
                    y = r * Math.sin(phi) * Math.sin(theta);
                    z = r * Math.cos(phi);
                } else {
                    // Ring (Flat disc with hole)
                    const rInner = 8;
                    const rOuter = 14;
                    const r = Math.sqrt(Math.random() * (rOuter*rOuter - rInner*rInner) + rInner*rInner);
                    x = r * Math.cos(theta);
                    z = r * Math.sin(theta); // 环在水平面(这里让它倾斜一点)
                    y = (Math.random() - 0.5) * 0.5; // 薄环

                    // 倾斜环
                    const tilt = 0.4; // 弧度
                    const yt = y * Math.cos(tilt) - z * Math.sin(tilt);
                    const zt = y * Math.sin(tilt) + z * Math.cos(tilt);
                    y = yt; z = zt;
                }
                break;

            case 'buddha':
                // 简化的禅修人像 (Procedural Composition)
                // 头部(球) + 身体(椭球) + 盘腿(圆环/椭圆)
                {
                    const dice = Math.random();
                    if (dice < 0.15) {
                        // Head
                        const r = 2.5 * Math.cbrt(Math.random());
                        x = r * Math.sin(phi) * Math.cos(theta);
                        y = r * Math.sin(phi) * Math.sin(theta) + 6; // Lift up
                        z = r * Math.cos(phi);
                    } else if (dice < 0.55) {
                        // Body
                        const r = 4.0 * Math.cbrt(Math.random());
                        x = r * Math.sin(phi) * Math.cos(theta);
                        y = (r * Math.sin(phi) * Math.sin(theta)) * 1.2; // Stretch Y slightly
                        z = r * Math.cos(phi) * 0.8; // Flatten Z
                    } else {
                        // Legs/Base
                        const r = 5.5 * Math.sqrt(Math.random());
                        x = r * Math.cos(theta);
                        z = r * Math.sin(theta) * 0.7;
                        y = (Math.random() - 1) * 2 - 3; // Bottom area
                    }
                }
                break;

            case 'firework':
                // 爆炸形态 (Burst)
                {
                    const r = 15 * Math.cbrt(Math.random());
                    // 集中在中心,也有边缘散落
                    x = r * Math.sin(phi) * Math.cos(theta);
                    y = r * Math.sin(phi) * Math.sin(theta);
                    z = r * Math.cos(phi);

                    // 增加一些射线效果
                    if (Math.random() > 0.9) {
                        x *= 1.5; y *= 1.5; z *= 1.5;
                    }
                }
                break;

            default:
                x = y = z = 0;
        }

        return { x, y, z };
    }

    function generateShape(shapeType) {
        currentShape = shapeType;

        for (let i = 0; i < PARTICLE_COUNT; i++) {
            const p = getPointOnShape(shapeType, i, PARTICLE_COUNT);
            basePositions[i * 3] = p.x;
            basePositions[i * 3 + 1] = p.y;
            basePositions[i * 3 + 2] = p.z;
        }

        // 当切换模型时,我们重置一下基础位置,但在动画循环中会应用手势缩放
        // 目标位置的更新逻辑主要在 animate 函数中结合手势系数实时计算
    }

    // ================= MediaPipe Hands 设置 =================
    const videoElement = document.getElementById('input_video');

    function onHandsResults(results) {
        // 更新预览 Canvas
        previewCanvas.width = videoElement.videoWidth;
        previewCanvas.height = videoElement.videoHeight;
        previewCtx.save();
        previewCtx.clearRect(0, 0, previewCanvas.width, previewCanvas.height);
        previewCtx.drawImage(results.image, 0, 0, previewCanvas.width, previewCanvas.height);

        if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
            isHandDetected = true;
            statusEl.innerText = "已检测到手势 - 尝试张合手掌";
            statusEl.style.color = "#00ff00";

            // 绘制骨架(可选,为了性能只绘制第一只手)
            drawConnectors(previewCtx, results.multiHandLandmarks[0], HAND_CONNECTIONS, {color: '#00FF00', lineWidth: 2});

            // --- 计算手势张合度 ---
            const landmarks = results.multiHandLandmarks[0];

            // 关键点: 0(手腕), 4(拇指尖), 8(食指尖)
            const wrist = landmarks[0];
            const thumbTip = landmarks[4];
            const indexTip = landmarks[8];
            const indexBase = landmarks[5]; // 食指根部

            // 计算参考长度(手掌大小):手腕到食指根部的距离
            // 用于归一化,解决手离摄像头远近导致的距离变化
            const palmSize = Math.sqrt(
                Math.pow(indexBase.x - wrist.x, 2) +
                Math.pow(indexBase.y - wrist.y, 2)
            );

            // 计算拇指尖到食指尖的距离
            const pinchDist = Math.sqrt(
                Math.pow(thumbTip.x - indexTip.x, 2) +
                Math.pow(thumbTip.y - indexTip.y, 2)
            );

            // 归一化张合程度 (Ratio)
            // 通常握拳时 Ratio < 0.2,张开时 Ratio > 1.0
            let openRatio = pinchDist / (palmSize || 0.1); // 防止除零

            // 映射到缩放因子
            // 我们设定:握拳(0.2) -> 缩放 0.4倍 | 张开(1.2) -> 缩放 2.5倍
            const minRatio = 0.2;
            const maxRatio = 1.2;
            const minScale = 0.3; // 最小收缩
            const maxScale = 2.5; // 最大扩散

            // Clamp and Map
            let normalized = (openRatio - minRatio) / (maxRatio - minRatio);
            if (normalized < 0) normalized = 0;
            if (normalized > 1) normalized = 1;

            // 线性插值得到目标缩放
            targetHandScale = minScale + normalized * (maxScale - minScale);

            // UI Update
            handStateEl.innerText = normalized < 0.2 ? "合拢" : (normalized > 0.8 ? "张开" : "运动中");

        } else {
            isHandDetected = false;
            statusEl.innerText = "未检测到手势,请在摄像头前举起单手";
            statusEl.style.color = "#00d2ff";
            handStateEl.innerText = "未检测";
            // 如果没检测到手,缓慢恢复到默认大小
            targetHandScale = 1.0;
        }
        previewCtx.restore();
    }

    const hands = new Hands({locateFile: (file) => {
        return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`;
    }});

    hands.setOptions({
        maxNumHands: 1,
        modelComplexity: 1, // 1 is Lite, faster.
        minDetectionConfidence: 0.5,
        minTrackingConfidence: 0.5
    });

    hands.onResults(onHandsResults);

    // 开启摄像头
    const cameraUtils = new Camera(videoElement, {
        onFrame: async () => {
            await hands.send({image: videoElement});
        },
        width: 320, // 低分辨率处理以提高性能
        height: 240
    });

    // 启动处理(需在此之后初始化)
    cameraUtils.start().catch(err => {
        statusEl.innerText = "摄像头访问失败,请检查权限。";
        statusEl.style.color = "red";
    });

    // ================= UI 交互逻辑 =================
    // 形状切换
    document.getElementById('shape-buttons').addEventListener('click', (e) => {
        if (e.target.classList.contains('shape-btn')) {
            // UI 更新
            document.querySelectorAll('.shape-btn').forEach(b => b.classList.remove('active'));
            e.target.classList.add('active');

            // 逻辑更新
            const shape = e.target.getAttribute('data-shape');
            generateShape(shape);
        }
    });

    // 颜色切换
    const colorPicker = document.getElementById('color-picker');
    colorPicker.addEventListener('input', (e) => {
        currentColor.set(e.target.value);
        if (material) material.color = currentColor;
    });

    // 全屏切换
    document.getElementById('fullscreen-btn').addEventListener('click', () => {
        if (!document.fullscreenElement) {
            document.documentElement.requestFullscreen();
        } else {
            if (document.exitFullscreen) {
                document.exitFullscreen();
            }
        }
    });

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

        // 1. 平滑手势缩放系数 (Lerp)
        // 延迟 <= 100ms 意味着我们需要较快的响应速度
        // simple lerp: current += (target - current) * speed
        handScaleFactor += (targetHandScale - handScaleFactor) * HAND_SENSITIVITY;

        // 更新UI显示数值
        scaleValEl.innerText = handScaleFactor.toFixed(2);

        // 2. 更新粒子位置
        const positions = particles.geometry.attributes.position.array;

        // 缓慢旋转整体
        particles.rotation.y += 0.002;
        // 如果是土星,稍微倾斜一点旋转
        if (currentShape === 'saturn') {
            particles.rotation.z = 0.3;
        } else {
            particles.rotation.z += (0 - particles.rotation.z) * 0.05; // 复位
        }

        for (let i = 0; i < PARTICLE_COUNT; i++) {
            const ix = i * 3;
            const iy = i * 3 + 1;
            const iz = i * 3 + 2;

            // 获取基础形状的目标位置
            const bx = basePositions[ix];
            const by = basePositions[iy];
            const bz = basePositions[iz];

            // 应用手势缩放
            // 扩散效果:位置 * 缩放系数
            const tx = bx * handScaleFactor;
            const ty = by * handScaleFactor;
            const tz = bz * handScaleFactor;

            // 粒子插值移动 (Morphing transition)
            // 这里同时处理了 "形状切换的过渡" 和 "手势缩放的实时性"
            // positions[ix] 是当前渲染位置
            positions[ix] += (tx - positions[ix]) * TRANSITION_SPEED;
            positions[iy] += (ty - positions[iy]) * TRANSITION_SPEED;
            positions[iz] += (tz - positions[iz]) * TRANSITION_SPEED;
        }

        particles.geometry.attributes.position.needsUpdate = true;

        renderer.render(scene, camera);
    }

    // 启动
    initThree();
    animate();

</script>
</body>
</html>

👉 你上面贴出的代码就是最终可运行版本

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇