文章封面

2026 爆火酷炫 3D 粒子交互,AI 3D粒子动态交互,手势跟随,

本人亲测苹果,安卓,电脑全部可行,如果微信打开网址不能跟随换夸克,google浏览器或系统默认浏览器测试.

测试链接: https://dh.dj3c.com//data/Lzjh.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: min(360px, calc(100vw - 40px));
            max-height: calc(100vh - 40px);
            overflow: hidden;
            background: rgba(30, 30, 30, 0.85);
            backdrop-filter: blur(10px);
            padding: 16px;
            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: 14px; }
        label { display: block; margin-bottom: 8px; font-size: 12px; color: #aaa; text-transform: uppercase; }

        #shape-buttons {
            display: grid;
            grid-template-columns: repeat(3, minmax(0, 1fr));
            gap: 7px;
        }

        /* 按钮样式 */
        .shape-btn {
            display: block;
            width: 100%;
            min-height: 34px;
            padding: 7px 4px;
            margin: 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: 12px;
            transition: all 0.2s;
            text-align: center;
            line-height: 1.2;
        }
        .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); /* 镜像 */
        }

        .gesture-guide {
            margin-top: 14px;
            padding-top: 12px;
            border-top: 1px solid rgba(255,255,255,0.1);
            display: grid;
            gap: 7px;
            max-height: min(260px, calc(100vh - 430px));
            overflow-y: auto;
            padding-right: 4px;
            font-size: 12px;
            color: rgba(255,255,255,0.76);
        }

        .gesture-guide::-webkit-scrollbar { width: 5px; }
        .gesture-guide::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.22); border-radius: 6px; }

        .gesture-row {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 10px;
            line-height: 1.35;
        }

        .gesture-row b {
            color: #7df9ff;
            font-weight: 600;
            white-space: nowrap;
        }
    </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 class="shape-btn" data-shape="portal">星门</div>
                <div class="shape-btn" data-shape="sword">剑阵</div>
                <div class="shape-btn" data-shape="beam">光柱</div>
                <div class="shape-btn" data-shape="spiral">星河</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 class="gesture-guide">
                <div class="gesture-row"><b>张掌</b><span>当前模型扩散</span></div>
                <div class="gesture-row"><b>握拳</b><span>当前模型压缩</span></div>
                <div class="gesture-row"><b>捏合</b><span>聚能收束</span></div>
                <div class="gesture-row"><b>双指</b><span>锐化展开</span></div>
                <div class="gesture-row"><b>拇指</b><span>能量升腾</span></div>
                <div class="gesture-row"><b>挥手</b><span>旋转加速</span></div>
                <div class="gesture-row"><b>双手靠近</b><span>收束旋涡</span></div>
                <div class="gesture-row"><b>双手拉开</b><span>空间展开</span></div>
            </div>
        </div>
    </div>

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

    // ================= 全局变量 =================
    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;
    let lastPalmX = null;
    let waveUntil = 0;
    let currentGestureName = '待机';
    let gestureCandidate = '';
    let gestureCandidateCount = 0;
    let targetRotationBoost = 0.002;
    let rotationBoost = 0.002;

    // 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(Math.min(window.devicePixelRatio, 1.5));
        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;

            case 'portal':
                // 星门:多层椭圆环
                {
                    const ring = Math.floor(Math.random() * 4);
                    const r = 6 + ring * 2.4 + Math.sin(theta * 6) * 0.5;
                    x = r * Math.cos(theta);
                    y = r * Math.sin(theta) * 0.72;
                    z = (Math.random() - 0.5) * 1.4 + Math.sin(theta * 3) * 0.4;
                }
                break;

            case 'sword':
                // 剑阵:多条平行剑光
                {
                    const lane = Math.floor(Math.random() * 7) - 3;
                    const t = Math.random() * 2 - 1;
                    x = t * 12;
                    y = lane * 1.2 + Math.sin(t * Math.PI * 4 + lane) * 0.45;
                    z = (Math.random() - 0.5) * 2.4;
                    if (Math.random() > 0.88) {
                        y += 2.8;
                        x *= 0.72;
                    }
                }
                break;

            case 'beam':
                // 光柱:竖向能量束
                {
                    const h = Math.random() * 18 - 9;
                    const radius = 0.6 + Math.pow(Math.random(), 2) * 3.4;
                    x = Math.cos(theta) * radius;
                    z = Math.sin(theta) * radius;
                    y = h + Math.sin(h * 2.2) * 0.4;
                }
                break;

            case 'spiral':
                // 星河:双螺旋飞掠
                {
                    const arm = Math.random() > 0.5 ? 1 : -1;
                    const t = Math.random() * Math.PI * 5;
                    const r = 0.8 + t * 0.85;
                    x = Math.cos(t * arm) * r;
                    z = Math.sin(t * arm) * r;
                    y = (Math.random() - 0.5) * 5 + Math.sin(t) * 1.2;
                }
                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 dist(a, b) {
        const dx = a.x - b.x;
        const dy = a.y - b.y;
        return Math.sqrt(dx * dx + dy * dy);
    }

    function fingerOpen(landmarks, tip, pip) {
        return landmarks[tip].y < landmarks[pip].y - 0.018;
    }

    function thumbOpen(landmarks) {
        const palmWidth = Math.max(0.001, dist(landmarks[5], landmarks[17]));
        return dist(landmarks[4], landmarks[9]) > palmWidth * 0.72 && dist(landmarks[4], landmarks[3]) > palmWidth * 0.18;
    }

    function analyzeHand(landmarks) {
        const thumb = thumbOpen(landmarks);
        const index = fingerOpen(landmarks, 8, 6);
        const middle = fingerOpen(landmarks, 12, 10);
        const ring = fingerOpen(landmarks, 16, 14);
        const pinky = fingerOpen(landmarks, 20, 18);
        const openCount = [thumb, index, middle, ring, pinky].filter(Boolean).length;
        const palmWidth = Math.max(0.001, dist(landmarks[5], landmarks[17]));
        const pinch = dist(landmarks[4], landmarks[8]) < palmWidth * 0.42;

        return {
            openCount,
            pinch,
            fist: openCount === 0,
            openPalm: openCount >= 4,
            twoFinger: index && middle && !ring && !pinky,
            thumbUp: thumb && !index && !middle && !ring && !pinky
        };
    }

    function setActiveShape(shape) {
        if (shape === currentShape) return;
        generateShape(shape);
        document.querySelectorAll('.shape-btn').forEach(btn => {
            btn.classList.toggle('active', btn.getAttribute('data-shape') === shape);
        });
    }

    function applyGesture(name, scale, spin = 0.002, stableFrames = 1) {
        if (name !== gestureCandidate) {
            gestureCandidate = name;
            gestureCandidateCount = 1;
            if (stableFrames > 1) return;
        } else {
            gestureCandidateCount++;
            if (gestureCandidateCount < stableFrames) return;
        }

        currentGestureName = name;
        targetHandScale = scale;
        targetRotationBoost = spin;
        handStateEl.innerText = name;
    }

    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];
            const now = performance.now();

            if (results.multiHandLandmarks.length >= 2) {
                const leftPalm = results.multiHandLandmarks[0][9];
                const rightPalm = results.multiHandLandmarks[1][9];
                const handGap = dist(leftPalm, rightPalm);
                if (handGap < 0.24) {
                    applyGesture('双手靠近:收束旋涡', 0.52, 0.024, 1);
                } else if (handGap > 0.46) {
                    applyGesture('双手拉开:空间展开', 2.35, 0.012, 1);
                }
            } else {
                const signal = analyzeHand(landmarks);
                const palmX = landmarks[9].x;

                if (lastPalmX !== null && Math.abs(palmX - lastPalmX) > 0.11) {
                    waveUntil = now + 560;
                }
                lastPalmX = palmX;

                if (now < waveUntil) {
                    applyGesture('横向挥手:飞掠加速', 2.05, 0.032, 1);
                } else if (signal.pinch && !signal.openPalm) {
                    applyGesture('捏合:聚能收缩', 0.36, 0.008, 1);
                } else if (signal.thumbUp) {
                    applyGesture('拇指:能量升腾', 1.62, 0.014, 1);
                } else if (signal.twoFinger) {
                    applyGesture('双指:锐化展开', 1.55, 0.02, 1);
                } else if (signal.fist) {
                    applyGesture('握拳:核心压缩', 0.28, 0.028, 1);
                } else if (signal.openPalm) {
                    applyGesture('张掌:星云扩散', 2.45, 0.007, 1);
                }
            }

            scaleValEl.innerText = targetHandScale.toFixed(2);

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

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

    hands.setOptions({
        maxNumHands: 2,
        modelComplexity: 0,
        minDetectionConfidence: 0.55,
        minTrackingConfidence: 0.55
    });

    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;

        // 缓慢旋转整体,手势只改变当前模型的互动强度,不切换模型
        rotationBoost += (targetRotationBoost - rotationBoost) * 0.2;
        particles.rotation.y += rotationBoost;
        // 如果是土星,稍微倾斜一点旋转
        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>

AI 3D粒子动态交互,codex,gamini,提示词

  • 创建一个基本的3D场景,包括相机、光源和渲染器。
  • 创建粒子系统:使用Three.js的粒子系统功能,生成大量粒子,并为每个粒子设置位置、颜色和大小等属性。
  • 实现手势控制:结合手势识别技术,允许用户通过手势来控制粒子的行为,例如改变粒子的运动方向或大小。
  • 渲染和动画:使用动态循环,不断更新粒子的位置和状态,以实现动态效果。

------本页内容已结束,喜欢请分享------

感谢您的来访,获取更多精彩文章请收藏本站。

背景封面
站长QQ图像

微信扫一扫分享

打开微信「扫一扫」,扫描二维码分享给好友或朋友圈

微信分享二维码
二维码生成失败,已为你复制链接,请在微信中粘贴分享。