今天抖音上偶然刷到的,真是不错,自己也试着开发了一下。

预览

预览

在线体验

体验地址 请戳👇 强烈建议使用电脑体验,手机非常卡顿!
https://www.yjln.com/shareCode/TuXingLiZi.html

测试机器:M4 Pro 48G(最完美)、M4 16G(流畅)、iPhone 17 Pro (卡顿)

源代码

如果你的设备比较老旧,可以修改代码第511行,减少粒子的数量即可。

const particleCount = 1200000; // 粒子总数:120万

代码:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>粒子土星</title>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <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>

    <style>
        body {
            margin: 0;
            overflow: hidden;
            background-color: #000;
            font-family: 'Microsoft YaHei', 'Segoe UI', sans-serif; /* 增加中文字体支持 */
            color: white;
        }

        #canvas-container {
            width: 100vw;
            height: 100vh;
            position: absolute;
            top: 0;
            left: 0;
            z-index: 1;
            /* 极度深邃的宇宙背景,微弱的径向光晕 */
            background: radial-gradient(circle at center, #050505 0%, #0b0b10 100%);
        }

        #ui-layer {
            position: absolute;
            top: 30px;
            left: 30px;
            z-index: 10;
            pointer-events: none;
        }

        .glass-panel {
            background: rgba(5, 5, 5, 0.7);
            backdrop-filter: blur(12px);
            padding: 24px;
            border-radius: 2px;
            border-left: 2px solid #c5a059;
            box-shadow: 0 10px 40px rgba(0, 0, 0, 0.9);
            max-width: 280px;
        }

        h1 {
            font-weight: 200;
            font-size: 1.8rem;
            margin: 0 0 10px 0;
            color: #e0cda7;
            letter-spacing: 4px;
            text-transform: uppercase;
        }

        .status-text {
            font-size: 0.85rem; /* 稍微调大一点适应中文 */
            color: #888;
            line-height: 1.6;
            font-family: monospace;
        }

        .highlight {
            color: #c5a059;
            font-weight: bold;
        }

        #controls {
            position: absolute;
            bottom: 40px;
            right: 40px;
            z-index: 10;
            pointer-events: auto;
        }

        /* 作者个人网站悬浮按钮 */
        #author-btn {
            position: absolute;
            top: 40px;
            right: 40px;
            z-index: 20;
            pointer-events: auto;
            text-decoration: none;
            color: #c5a059;
            border: 1px solid rgba(197, 160, 89, 0.4);
            background: rgba(0, 0, 0, 0.6);
            padding: 8px 20px;
            font-size: 0.8rem;
            border-radius: 20px;
            transition: all 0.3s ease;
            backdrop-filter: blur(4px);
            letter-spacing: 1px;
            font-family: monospace;
        }

        #author-btn:hover {
            background: #c5a059;
            color: #000;
            box-shadow: 0 0 15px rgba(197, 160, 89, 0.4);
        }

        button {
            background: transparent;
            border: 1px solid rgba(197, 160, 89, 0.3);
            color: #c5a059;
            padding: 12px 30px;
            font-size: 0.8rem;
            cursor: pointer;
            transition: all 0.4s ease;
            text-transform: uppercase;
            letter-spacing: 2px;
        }

        button:hover {
            background: rgba(197, 160, 89, 0.1);
            border-color: #c5a059;
            box-shadow: 0 0 15px rgba(197, 160, 89, 0.2);
        }

        #loading {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            z-index: 20;
            text-align: center;
            color: #444;
            font-size: 0.8rem;
            letter-spacing: 3px;
            text-transform: uppercase;
        }
        
        #fps-counter {
            position: absolute;
            top: 10px;
            right: 10px;
            color: #333;
            font-family: monospace;
            font-size: 10px;
            z-index: 5;
        }

        .input_video { display: none; }
    </style>
</head>
<body>

    <video class="input_video"></video>
    
    <!-- 作者跳转按钮 -->
    <a id="author-btn" href="https://www.yjln.com" target="_blank">By Mr.lun</a>

    <div id="fps-counter">物理引擎: 极致模式 | 环境: 太阳系模拟</div>

    <div id="loading">
        正在构建百万粒子与行星数据...
    </div>

    <div id="ui-layer">
        <div class="glass-panel">
            <h1>土星</h1>
            <div class="status-text">
                数据流状态: <span id="status-indicator" class="highlight">待机</span>
                <br><br>
                > 开普勒轨道: 运行中<br>
                > 粒子总数: 120万+<br>
                > 背景环境: 行星已加载
            </div>
        </div>
    </div>

    <div id="controls">
        <button onclick="toggleFullScreen()">全屏沉浸体验</button>
    </div>

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

    <!-- 1. 土星 顶点着色器 (处理粒子位置/大小/噪点) -->
    <script type="x-shader/x-vertex" id="vertexshader">
        attribute float size;
        attribute vec3 customColor;
        attribute float opacityAttr;
        attribute float orbitSpeed;
        attribute float isRing;
        attribute float aRandomId;
        
        varying vec3 vColor;
        varying float vDist;
        varying float vOpacity;
        varying float vScaleFactor;
        varying float vIsRing;
        
        uniform float uTime;
        uniform float uScale;
        uniform float uRotationX;
        
        // 2D 旋转矩阵
        mat2 rotate2d(float _angle){
            return mat2(cos(_angle),-sin(_angle),
                        sin(_angle),cos(_angle));
        }

        // 简单的哈希函数,用来做随机
        float hash(float n) { return fract(sin(n) * 43758.5453123); }

        void main() {
            // 根据缩放级别做 LOD (细节层次) 剔除,离得远就不渲染那么多点
            float normScaleLOD = clamp((uScale - 0.15) / 2.35, 0.0, 1.0);
            float visibilityThreshold = 0.9 + pow(normScaleLOD, 1.2) * 0.1; 

            // 如果随机ID大于阈值,直接把点扔出屏幕,省显卡资源
            if (aRandomId > visibilityThreshold) {
                gl_Position = vec4(0.0);
                gl_PointSize = 0.0;
                return;
            }

            vec3 pos = position;

            // 让光环和本体分开旋转,增加动态感
            if (isRing > 0.5) {
                float angleOffset = uTime * orbitSpeed * 0.2;
                vec2 rotatedXZ = rotate2d(angleOffset) * pos.xz;
                pos.x = rotatedXZ.x;
                pos.z = rotatedXZ.y;
            } else {
                float bodyAngle = uTime * 0.03;
                vec2 rotatedXZ = rotate2d(bodyAngle) * pos.xz;
                pos.x = rotatedXZ.x;
                pos.z = rotatedXZ.y;
            }

            // 处理整体视角的 X 轴旋转(即手势控制的俯仰角)
            float cx = cos(uRotationX);
            float sx = sin(uRotationX);
            float ry = pos.y * cx - pos.z * sx;
            float rz = pos.y * sx + pos.z * cx;
            pos.y = ry;
            pos.z = rz;

            // 转换到相机空间
            vec4 mvPosition = modelViewMatrix * vec4(pos * uScale, 1.0);
            float dist = -mvPosition.z;
            vDist = dist;

            // --- 混沌噪点效果 ---
            // 当摄像机贴得很近时,让粒子位置产生抖动,模拟气体湍流
            float chaosThreshold = 25.0; 
            if (dist < chaosThreshold && dist > 0.1) {
                float chaosIntensity = 1.0 - (dist / chaosThreshold);
                chaosIntensity = pow(chaosIntensity, 3.0);

                float highFreqTime = uTime * 40.0; 
                float noiseX = sin(highFreqTime + pos.x * 10.0) * hash(pos.y);
                float noiseY = cos(highFreqTime + pos.y * 10.0) * hash(pos.x);
                float noiseZ = sin(highFreqTime * 0.5) * hash(pos.z);
                
                vec3 noiseVec = vec3(noiseX, noiseY, noiseZ) * chaosIntensity * 3.0;
                mvPosition.xyz += noiseVec;
            }
            
            gl_Position = projectionMatrix * mvPosition;
            
            // 根据距离计算粒子大小 (透视投影)
            float pointSize = size * (350.0 / dist); 
            pointSize *= 0.55; 

            // 近距离观察行星本体时,稍微把点变小一点,看起来更细腻
            if (isRing < 0.5 && dist < 50.0) {
                 pointSize *= 0.8; 
            }

            gl_PointSize = clamp(pointSize, 0.0, 300.0);

            // 传递数据给片元着色器
            vColor = customColor;
            vOpacity = opacityAttr;
            vScaleFactor = uScale;
            vIsRing = isRing;
        }
    </script>

    <!-- 2. 土星 片元着色器 (处理颜色/光晕/材质) -->
    <script type="x-shader/x-fragment" id="fragmentshader">
        varying vec3 vColor;
        varying float vDist;
        varying float vOpacity;
        varying float vScaleFactor;
        varying float vIsRing;
        
        void main() {
            // 把方形的点变成圆的
            vec2 cxy = 2.0 * gl_PointCoord - 1.0;
            float r = dot(cxy, cxy);
            if (r > 1.0) discard;
            
            // 边缘羽化,做成光球的效果
            float glow = smoothstep(1.0, 0.4, r); 
            
            // 根据缩放比例计算一个过渡值
            float t = clamp((vScaleFactor - 0.15) / 2.35, 0.0, 1.0);

            // 颜色混合逻辑:放大时偏向原色,缩小时偏向深金色
            vec3 deepGold = vec3(0.35, 0.22, 0.05); 
            float colorMix = smoothstep(0.1, 0.9, t);
            vec3 baseColor = mix(deepGold, vColor, colorMix);

            float brightness = 0.2 + 1.0 * t; 

            // 密度透明度调整
            float densityAlpha = 0.25 + 0.45 * smoothstep(0.0, 0.5, t);

            vec3 finalColor = baseColor * brightness;
            
            // --- 近距离纹理增强 ---
            if (vDist < 40.0) {
                float closeMix = 1.0 - (vDist / 40.0);
                
                if (vIsRing < 0.5) {
                    // 行星本体:增加一点对比度和深色纹理
                    vec3 deepTexture = pow(vColor, vec3(1.4)) * 1.5; 
                    finalColor = mix(finalColor, deepTexture, closeMix * 0.8);
                } else {
                    // 光环:增加一点尘埃感
                    finalColor += vec3(0.15, 0.12, 0.1) * closeMix;
                }
            }

            // 防止近裁切面太生硬,淡出
            float depthAlpha = 1.0;
            if (vDist < 10.0) depthAlpha = smoothstep(0.0, 10.0, vDist);

            float alpha = glow * vOpacity * densityAlpha * depthAlpha;
            
            gl_FragColor = vec4(finalColor, alpha);
        }
    </script>

    <!-- 3. 背景星空 顶点着色器 -->
    <script type="x-shader/x-vertex" id="starVertexShader">
        attribute float size;
        attribute vec3 customColor;
        varying vec3 vColor;
        uniform float uTime;

        void main() {
            vColor = customColor;
            vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
            float dist = -mvPosition.z;
            // 星星不需要太大,这里限制一下大小
            gl_PointSize = size * (1000.0 / dist); 
            gl_PointSize = clamp(gl_PointSize, 1.0, 8.0);
            gl_Position = projectionMatrix * mvPosition;
        }
    </script>

    <!-- 4. 背景星空 片元着色器 -->
    <script type="x-shader/x-fragment" id="starFragmentShader">
        varying vec3 vColor;
        uniform float uTime;
        float random(vec2 st) { return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123); }
        void main() {
            vec2 cxy = 2.0 * gl_PointCoord - 1.0;
            float r = dot(cxy, cxy);
            if (r > 1.0) discard;
            
            // 模拟星星闪烁
            float noise = random(gl_FragCoord.xy);
            float twinkle = 0.7 + 0.3 * sin(uTime * 2.0 + noise * 10.0);
            
            float glow = 1.0 - r;
            glow = pow(glow, 1.5);
            
            gl_FragColor = vec4(vColor * twinkle, glow * 0.8);
        }
    </script>

    <!-- 5. 行星 顶点着色器 (新) -->
    <script type="x-shader/x-vertex" id="planetVertexShader">
        varying vec2 vUv;
        varying vec3 vNormal;
        varying vec3 vViewPosition;
        void main() {
            vUv = uv;
            vNormal = normalize(normalMatrix * normal);
            vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
            vViewPosition = -mvPosition.xyz;
            gl_Position = projectionMatrix * mvPosition;
        }
    </script>

    <!-- 6. 行星 片元着色器 (程序化生成纹理) -->
    <script type="x-shader/x-fragment" id="planetFragmentShader">
        uniform vec3 color1;
        uniform vec3 color2;
        uniform float noiseScale;
        uniform vec3 lightDir;
        uniform float atmosphere;
        
        varying vec2 vUv;
        varying vec3 vNormal;
        varying vec3 vViewPosition;

        // 基础噪声函数
        float random(vec2 st) { return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123); }
        float noise(vec2 st) {
            vec2 i = floor(st);
            vec2 f = fract(st);
            float a = random(i);
            float b = random(i + vec2(1.0, 0.0));
            float c = random(i + vec2(0.0, 1.0));
            float d = random(i + vec2(1.0, 1.0));
            vec2 u = f * f * (3.0 - 2.0 * f);
            return mix(a, b, u.x) + (c - a)* u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
        }
        
        // 分形布朗运动 (FBM) - 用来生成地形或云层纹理
        float fbm(vec2 st) {
            float value = 0.0;
            float amplitude = 0.5;
            for (int i = 0; i < 5; i++) {
                value += amplitude * noise(st);
                st *= 2.0;
                amplitude *= 0.5;
            }
            return value;
        }

        void main() {
            // 根据噪声生成地表颜色
            float n = fbm(vUv * noiseScale);
            vec3 albedo = mix(color1, color2, n);
            
            // 简单的漫反射光照
            vec3 normal = normalize(vNormal);
            vec3 light = normalize(lightDir);
            float diff = max(dot(normal, light), 0.05);
            
            // 菲涅尔效应 (Fresnel) - 用来模拟大气层边缘发光
            vec3 viewDir = normalize(vViewPosition);
            float fresnel = pow(1.0 - dot(viewDir, normal), 3.0);
            
            vec3 finalColor = albedo * diff + atmosphere * vec3(0.5, 0.6, 1.0) * fresnel;
            
            gl_FragColor = vec4(finalColor, 1.0);
        }
    </script>

    <script>
        let scene, camera, renderer, particles, stars, nebula;
        let planetGroup; // 存储背景行星组
        let uniforms, starUniforms;
        
        // 目标值 vs 当前值 (用于平滑动画)
        let targetScale = 1.0;
        let targetRotX = 0.4;
        let currentScale = 1.0;
        let currentRotX = 0.4;
        
        let isHandDetected = false;
        
        const videoElement = document.getElementsByClassName('input_video')[0];
        const statusElement = document.getElementById('status-indicator');
        const loadingElement = document.getElementById('loading');

        function initThree() {
            const container = document.getElementById('canvas-container');

            scene = new THREE.Scene();
            // 远处的雾,增加深邃感
            scene.fog = new THREE.FogExp2(0x020202, 0.00015);

            camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 10000);
            camera.position.z = 100;
            camera.lookAt(0, 0, 0);

            // 1. 初始化土星系统 (120万粒子)
            initSaturn();
            
            // 2. 初始化背景星空 (5万粒子 + 星云)
            initStarfield();

            // 3. 初始化背景实体行星 (地球、火星、水星)
            initPlanets();

            renderer = new THREE.WebGLRenderer({ 
                antialias: true, 
                alpha: true,
                powerPreference: "high-performance" // 告诉浏览器尽量用独显
            });
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // 限制像素比,防止高分屏卡顿
            renderer.setClearColor(0x000000, 0); 
            container.appendChild(renderer.domElement);

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

            animate();
        }

        function initSaturn() {
            const particleCount = 1200000; // 粒子总数:120万
            const geometry = new THREE.BufferGeometry();
            
            // 申请内存
            const positions = new Float32Array(particleCount * 3);
            const colors = new Float32Array(particleCount * 3);
            const sizes = new Float32Array(particleCount);
            const opacities = new Float32Array(particleCount);
            const orbitSpeeds = new Float32Array(particleCount);
            const isRings = new Float32Array(particleCount);
            const randomIds = new Float32Array(particleCount);

            // 土星本体的色调
            const bodyColors = [
                new THREE.Color('#E3DAC5'), 
                new THREE.Color('#C9A070'), 
                new THREE.Color('#E3DAC5'), 
                new THREE.Color('#B08D55')  
            ];
            
            // 土星环的各层颜色 (参考卡西尼号数据)
            const colorRingC = new THREE.Color('#2A2520'); 
            const colorRingB_Inner = new THREE.Color('#CDBFA0'); 
            const colorRingB_Outer = new THREE.Color('#DCCBBA'); 
            const colorCassini = new THREE.Color('#050505'); // 卡西尼缝 (黑的)
            const colorRingA = new THREE.Color('#989085'); 
            const colorRingF = new THREE.Color('#AFAFA0'); 

            const R_PLANET = 18; // 行星基础半径

            for(let i = 0; i < particleCount; i++) {
                let x, y, z, r, g, b, size, opacity, speed, isRingVal;
                randomIds[i] = Math.random();

                // 前 25% 的粒子用来画土星本体
                if (i < particleCount * 0.25) {
                    isRingVal = 0.0;
                    speed = 0.0;
                    const u = Math.random();
                    const v = Math.random();
                    const theta = 2 * Math.PI * u;
                    const phi = Math.acos(2 * v - 1);
                    const rad = R_PLANET;
                    
                    x = rad * Math.sin(phi) * Math.cos(theta);
                    let rawY = rad * Math.cos(phi);
                    z = rad * Math.sin(phi) * Math.sin(theta);
                    
                    // 土星是扁的,压扁一点 Y 轴
                    y = rawY * 0.9;
                    
                    // 生成条纹图案
                    let lat = (rawY / rad + 1.0) * 0.5; 
                    let bandNoise = Math.cos(lat * 40.0) * 0.8 + Math.cos(lat * 15.0) * 0.4;
                    let colIndex = Math.floor(lat * 4 + bandNoise) % 4;
                    if (colIndex < 0) colIndex = 0;
                    let baseCol = bodyColors[colIndex];
                    
                    r = baseCol.r; g = baseCol.g; b = baseCol.b;
                    size = 1.0 + Math.random() * 0.8; 
                    opacity = 0.8; 
                } else {
                    // 剩下的粒子画光环
                    isRingVal = 1.0;
                    let zoneRand = Math.random();
                    let ringRadius;
                    let ringCol;
                    
                    // 根据概率分布生成不同的环带 (C环, B环, 卡西尼缝, A环, F环)
                    if (zoneRand < 0.15) { 
                        // C环: 较暗,较内层
                        ringRadius = R_PLANET * (1.235 + Math.random() * (1.525 - 1.235));
                        ringCol = colorRingC;
                        size = 0.5; opacity = 0.3; 
                    } else if (zoneRand < 0.65) { 
                        // B环: 最亮,最宽
                        let t = Math.random();
                        ringRadius = R_PLANET * (1.525 + t * (1.95 - 1.525));
                        ringCol = colorRingB_Inner.clone().lerp(colorRingB_Outer, t);
                        size = 0.8 + Math.random() * 0.6; opacity = 0.85; 
                        // B环有些地方密度特高
                        if (Math.sin(ringRadius * 2.0) > 0.8) opacity *= 1.2;
                    } else if (zoneRand < 0.69) { 
                        // 卡西尼缝: 几乎是空的,粒子很少且暗
                        ringRadius = R_PLANET * (1.95 + Math.random() * (2.025 - 1.95));
                        ringCol = colorCassini;
                        size = 0.3; opacity = 0.1; 
                    } else if (zoneRand < 0.99) { 
                        // A环
                        ringRadius = R_PLANET * (2.025 + Math.random() * (2.27 - 2.025));
                        ringCol = colorRingA;
                        size = 0.7; opacity = 0.6;
                        // 恩克环缝
                        if (ringRadius > R_PLANET * 2.2 && ringRadius < R_PLANET * 2.21) opacity = 0.1;
                    } else { 
                        // F环: 最外层,很细
                        ringRadius = R_PLANET * (2.32 + Math.random() * 0.02);
                        ringCol = colorRingF;
                        size = 1.0; opacity = 0.7;
                    }
                    
                    const theta = Math.random() * Math.PI * 2;
                    x = ringRadius * Math.cos(theta);
                    z = ringRadius * Math.sin(theta);
                    
                    // 环也是有厚度的,稍微随机一点 Y
                    let thickness = 0.15;
                    if (ringRadius > R_PLANET * 2.3) thickness = 0.4; 
                    y = (Math.random() - 0.5) * thickness;
                    
                    r = ringCol.r; g = ringCol.g; b = ringCol.b;
                    
                    // 开普勒第三定律:越外层转得越慢
                    speed = 8.0 / Math.sqrt(ringRadius);
                }
                
                positions[i*3] = x; positions[i*3+1] = y; positions[i*3+2] = z;
                colors[i*3] = r; colors[i*3+1] = g; colors[i*3+2] = b;
                sizes[i] = size; opacities[i] = opacity;
                orbitSpeeds[i] = speed; isRings[i] = isRingVal;
            }

            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('opacityAttr', new THREE.BufferAttribute(opacities, 1));
            geometry.setAttribute('orbitSpeed', new THREE.BufferAttribute(orbitSpeeds, 1));
            geometry.setAttribute('isRing', new THREE.BufferAttribute(isRings, 1));
            geometry.setAttribute('aRandomId', new THREE.BufferAttribute(randomIds, 1));

            uniforms = {
                uTime: { value: 0 },
                uScale: { value: 1.0 },
                uRotationX: { value: 0.4 }
            };

            const material = new THREE.ShaderMaterial({
                depthWrite: false, // 不写入深度缓冲区,防止粒子互相遮挡产生黑边
                blending: THREE.AdditiveBlending, // 叠加混合模式,让光环看起来更亮
                vertexColors: true,
                uniforms: uniforms,
                vertexShader: document.getElementById('vertexshader').textContent,
                fragmentShader: document.getElementById('fragmentshader').textContent,
                transparent: true
            });

            particles = new THREE.Points(geometry, material);
            particles.rotation.z = 26.73 * (Math.PI / 180); // 土星真实的轴倾角
            scene.add(particles);
        }

        function initStarfield() {
            const starCount = 50000;
            const geo = new THREE.BufferGeometry();
            const pos = new Float32Array(starCount * 3);
            const cols = new Float32Array(starCount * 3);
            const sizes = new Float32Array(starCount);
            
            // 星星颜色类型
            const starColors = [
                new THREE.Color('#9bb0ff'), new THREE.Color('#ffffff'), 
                new THREE.Color('#ffcc6f'), new THREE.Color('#ff7b7b')
            ];

            for(let i=0; i<starCount; i++) {
                const r = 400 + Math.random() * 3000; // 离远点
                const theta = Math.random() * Math.PI * 2;
                const phi = Math.acos(2 * Math.random() - 1);
                
                pos[i*3] = r * Math.sin(phi) * Math.cos(theta);
                pos[i*3+1] = r * Math.cos(phi);
                pos[i*3+2] = r * Math.sin(phi) * Math.sin(theta);
                
                const colorType = Math.random();
                let c;
                if(colorType > 0.9) c = starColors[0]; else if(colorType > 0.6) c = starColors[1];
                else if(colorType > 0.3) c = starColors[2]; else c = starColors[3];
                
                cols[i*3] = c.r; cols[i*3+1] = c.g; cols[i*3+2] = c.b;
                sizes[i] = 1.0 + Math.random() * 3.0;
            }
            geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
            geo.setAttribute('customColor', new THREE.BufferAttribute(cols, 3));
            geo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));

            starUniforms = { uTime: { value: 0 } };
            const mat = new THREE.ShaderMaterial({
                uniforms: starUniforms,
                vertexShader: document.getElementById('starVertexShader').textContent,
                fragmentShader: document.getElementById('starFragmentShader').textContent,
                transparent: true, depthWrite: false, blending: THREE.AdditiveBlending
            });
            stars = new THREE.Points(geo, mat);
            scene.add(stars);

            // --- 简单的星云效果 ---
            const nebulaCount = 100;
            const nebGeo = new THREE.BufferGeometry();
            const nebPos = new Float32Array(nebulaCount * 3);
            const nebCols = new Float32Array(nebulaCount * 3);
            const nebSizes = new Float32Array(nebulaCount);
            for(let i=0; i<nebulaCount; i++) {
                const r = 800 + Math.random() * 2000;
                const theta = Math.random() * Math.PI * 2;
                const phi = Math.PI / 2 + (Math.random() - 0.5) * 1.5; // 主要集中在赤道面上
                nebPos[i*3] = r * Math.sin(phi) * Math.cos(theta);
                nebPos[i*3+1] = r * Math.cos(phi);
                nebPos[i*3+2] = r * Math.sin(phi) * Math.sin(theta);
                const nc = new THREE.Color().setHSL(0.6 + Math.random()*0.2, 0.8, 0.05);
                nebCols[i*3] = nc.r; nebCols[i*3+1] = nc.g; nebCols[i*3+2] = nc.b;
                nebSizes[i] = 400.0 + Math.random() * 600.0;
            }
            nebGeo.setAttribute('position', new THREE.BufferAttribute(nebPos, 3));
            nebGeo.setAttribute('customColor', new THREE.BufferAttribute(nebCols, 3));
            nebGeo.setAttribute('size', new THREE.BufferAttribute(nebSizes, 1));
            const nebShaderMat = new THREE.ShaderMaterial({
                uniforms: {},
                vertexShader: document.getElementById('starVertexShader').textContent,
                fragmentShader: `
                    varying vec3 vColor;
                    void main() {
                        vec2 cxy = 2.0 * gl_PointCoord - 1.0;
                        float r = dot(cxy, cxy);
                        if(r > 1.0) discard;
                        float glow = pow(1.0 - r, 2.0);
                        gl_FragColor = vec4(vColor, glow * 0.1); 
                    }
                `,
                transparent: true, depthWrite: false, blending: THREE.AdditiveBlending
            });
            nebula = new THREE.Points(nebGeo, nebShaderMat);
            scene.add(nebula);
        }

        // --- 初始化背景行星 ---
        function initPlanets() {
            planetGroup = new THREE.Group();
            scene.add(planetGroup);

            const vShader = document.getElementById('planetVertexShader').textContent;
            const fShader = document.getElementById('planetFragmentShader').textContent;

            // 1. 火星 (Mars) - 红色,噪点多
            createPlanet(planetGroup, vShader, fShader, 
                new THREE.Color('#b33a00'), new THREE.Color('#d16830'), 8.0, 
                { x: -300, y: 120, z: -450 }, 10, 0.3 
            );

            // 2. 地球 (Earth) - 蓝白相间
            createPlanet(planetGroup, vShader, fShader, 
                new THREE.Color('#001e4d'), new THREE.Color('#ffffff'), 5.0, 
                { x: 380, y: -100, z: -600 }, 14, 0.6 
            );

            // 3. 水星 (Mercury) - 灰白,小
            createPlanet(planetGroup, vShader, fShader, 
                new THREE.Color('#666666'), new THREE.Color('#aaaaaa'), 15.0, 
                { x: -180, y: -220, z: -350 }, 6, 0.1 
            );
        }

        function createPlanet(group, vShader, fShader, c1, c2, nScale, pos, radius, atmo) {
            const geo = new THREE.SphereGeometry(radius, 48, 48);
            const mat = new THREE.ShaderMaterial({
                uniforms: {
                    color1: { value: c1 },
                    color2: { value: c2 },
                    noiseScale: { value: nScale },
                    lightDir: { value: new THREE.Vector3(1, 0.5, 1) },
                    atmosphere: { value: atmo }
                },
                vertexShader: vShader,
                fragmentShader: fShader
            });
            const mesh = new THREE.Mesh(geo, mat);
            mesh.position.set(pos.x, pos.y, pos.z);
            group.add(mesh);
        }

        const clock = new THREE.Clock();
        let autoIdleTime = 0;

        function animate() {
            requestAnimationFrame(animate);

            const elapsedTime = clock.getElapsedTime();
            uniforms.uTime.value = elapsedTime;
            if(starUniforms) starUniforms.uTime.value = elapsedTime;

            // 缓慢旋转背景星空
            if(stars) stars.rotation.y = elapsedTime * 0.005;
            if(nebula) nebula.rotation.y = elapsedTime * 0.003;
            
            // 行星自转
            if(planetGroup) {
                planetGroup.children.forEach((planet, idx) => {
                    planet.rotation.y = elapsedTime * (0.05 + idx * 0.02);
                });
                // 整个行星组稍微移动一点视差
                planetGroup.rotation.y = Math.sin(elapsedTime * 0.05) * 0.02;
            }

            // 如果没检测到手,就开启自动巡航演示模式
            if (!isHandDetected) {
                autoIdleTime += 0.005;
                targetScale = 1.0 + Math.sin(autoIdleTime) * 0.2;
                targetRotX = 0.4 + Math.sin(autoIdleTime * 0.3) * 0.15;
                
                statusElement.innerHTML = "系统状态: 自动巡航<br>输入信号: 等待中...";
                statusElement.style.color = "#666";
            } else {
                statusElement.innerHTML = "系统状态: 手动接管<br>输入信号: <span class='highlight'>已锁定</span>";
                statusElement.style.color = "#c5a059";
            }

            // 平滑插值 (Lerp),让动作更顺滑
            const lerpFactor = 0.08;
            currentScale += (targetScale - currentScale) * lerpFactor;
            currentRotX += (targetRotX - currentRotX) * lerpFactor;

            uniforms.uScale.value = currentScale;
            uniforms.uRotationX.value = currentRotX;

            renderer.render(scene, camera);
        }

        // 初始化手势追踪
        const hands = new Hands({locateFile: (file) => {
            return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`;
        }});

        hands.setOptions({
            maxNumHands: 1,
            modelComplexity: 1,
            minDetectionConfidence: 0.7,
            minTrackingConfidence: 0.7
        });

        hands.onResults(onResults);

        function onResults(results) {
            loadingElement.style.display = 'none';

            if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
                isHandDetected = true;
                const hand = results.multiHandLandmarks[0];

                // 用大拇指和食指的距离控制缩放
                const p1 = hand[4];
                const p2 = hand[8];
                const dist = Math.sqrt((p1.x-p2.x)**2 + (p1.y-p2.y)**2);
                
                // 归一化距离并映射到缩放系数
                const normDist = Math.max(0, Math.min(1, (dist - 0.02) / 0.25));
                targetScale = 0.15 + normDist * 2.35; 

                // 用手掌在屏幕的 Y 轴位置控制俯仰角
                const y = hand[9].y;
                const normY = Math.max(0, Math.min(1, (y - 0.1) / 0.8));
                targetRotX = -0.6 + normY * 1.6;

            } else {
                isHandDetected = false;
            }
        }

        // 启动摄像头
        const cameraUtils = new Camera(videoElement, {
            onFrame: async () => {
                await hands.send({image: videoElement});
            },
            width: 640,
            height: 480
        });
        
        cameraUtils.start().catch(e => {
            console.error(e);
            loadingElement.innerText = "摄像头启动失败";
        });

        function toggleFullScreen() {
            if (!document.fullscreenElement) {
                document.documentElement.requestFullscreen();
            } else if (document.exitFullscreen) {
                document.exitFullscreen();
            }
        }

        initThree();

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