超酷炫的Three.js示例

发布于:2025-08-18 ⋅ 阅读:(16) ⋅ 点赞:(0)

今天写一个超级酷炫的Three.js示例,以下是文件源代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Cool Three.js Page with Stars, Interactions, and Audio</title>
  <style>
    body { margin: 0; overflow: hidden; background-color: black; }
    canvas { display: block; }
    #info {
      position: absolute;
      top: 20px;
      left: 20px;
      color: white;
      font-family: Arial, sans-serif;
      font-size: 20px;
      z-index: 1;
    }
    audio {
      position: fixed;
      top: 20px;
      right: 20px;
      z-index: 10;
      width: 300px;
    }
  </style>
</head>
<body>
  <div id="info">🚀 Three.js Demo with Stars ✨ + Click/Audio FX</div>
  <audio id="audio" src="https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3" controls autoplay loop></audio>

  <!-- 使用兼容非模块版本的three.js和OrbitControls -->
  <script src="https://cdn.jsdelivr.net/npm/three@0.140.0/build/three.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/three@0.140.0/examples/js/controls/OrbitControls.js"></script>

  <script>
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      2000
    );
    camera.position.z = 100;

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

    // 注意这里用 THREE.OrbitControls(旧版写法)
    const controls = new THREE.OrbitControls(camera, renderer.domElement);

    // 着色器材质代码(glow效果)
    const vertexShader = `
      varying vec3 vNormal;
      void main() {
        vNormal = normalize(normalMatrix * normal);
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      }
    `;
    const fragmentShader = `
      varying vec3 vNormal;
      void main() {
        float intensity = pow(0.6 - dot(vNormal, vec3(0.0, 0.0, 1.0)), 2.0);
        gl_FragColor = vec4(0.0, 1.0, 1.0, 1.0) * intensity;
      }
    `;
    const shaderMaterial = new THREE.ShaderMaterial({
      vertexShader,
      fragmentShader,
      blending: THREE.AdditiveBlending,
      side: THREE.FrontSide,  // 改为 FrontSide
      transparent: true
    });

    const geometry = new THREE.IcosahedronGeometry(2, 1);
    const glowGroup = new THREE.Group();
    for (let i = 0; i < 200; i++) {
      const mesh = new THREE.Mesh(geometry, shaderMaterial);
      mesh.scale.multiplyScalar(1.5);
      mesh.position.set(
        (Math.random() - 0.5) * 400,
        (Math.random() - 0.5) * 400,
        (Math.random() - 0.5) * 400
      );
      glowGroup.add(mesh);
    }
    scene.add(glowGroup);

    // 星空背景粒子
    const starGeometry = new THREE.BufferGeometry();
    const starCount = 5000;
    const starVertices = [];
    for (let i = 0; i < starCount; i++) {
      starVertices.push((Math.random() - 0.5) * 2000);
      starVertices.push((Math.random() - 0.5) * 2000);
      starVertices.push((Math.random() - 0.5) * 2000);
    }
    starGeometry.setAttribute('position', new THREE.Float32BufferAttribute(starVertices, 3));
    const starMaterial = new THREE.PointsMaterial({ color: 0xffffff, size: 0.7 });
    const starField = new THREE.Points(starGeometry, starMaterial);
    scene.add(starField);

    // 灯光
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
    scene.add(ambientLight);
    const pointLight = new THREE.PointLight(0xffffff, 1);
    camera.add(pointLight);
    scene.add(camera);

    // 鼠标点击爆炸效果
    const raycaster = new THREE.Raycaster();
    const mouse = new THREE.Vector2();
    window.addEventListener('click', event => {
      mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
      mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
      raycaster.setFromCamera(mouse, camera);
      const intersects = raycaster.intersectObjects(glowGroup.children);
      if (intersects.length > 0) {
        const mesh = intersects[0].object;
        const explosion = new THREE.Vector3(
          (Math.random() - 0.5) * 100,
          (Math.random() - 0.5) * 100,
          (Math.random() - 0.5) * 100
        );
        mesh.position.add(explosion);
      }
    });

    // 音频分析器
    const audio = document.getElementById('audio');
    const listener = new THREE.AudioListener();
    camera.add(listener);
    const sound = new THREE.Audio(listener);
    const audioLoader = new THREE.AudioLoader();
    audioLoader.load(audio.src, buffer => {
      sound.setBuffer(buffer);
      sound.setLoop(true);
      sound.setVolume(0.5);
      sound.play();
    });
    const analyser = new THREE.AudioAnalyser(sound, 32);

    function animate() {
      requestAnimationFrame(animate);

      const data = analyser.getAverageFrequency();
      glowGroup.children.forEach((mesh, i) => {
        const scale = 1.5 + Math.sin(Date.now() * 0.001 + i) * 0.3 + data / 256;
        mesh.scale.set(scale, scale, scale);
      });

      glowGroup.rotation.y += 0.002;
      starField.rotation.y += 0.0005;

      renderer.render(scene, camera);
    }

    animate();

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

一、整体思路

  • 使用非模块版 Three.js(r140)与老写法的 THREE.OrbitControls
  • 场景里有两类物体:
    1. 200 个“发光”小多面体(用自定义 Shader 做到类似辉光的视觉)
    2. 5000 颗 Points 形式的星空粒子
  • 交互:点击“发光体”会被随机“炸开”移动一下。
  • 音频:加载一段 MP3,用 AudioAnalyser 得到频率均值,驱动 200 个发光体按音乐节奏伸缩。
  • 动画:群组及星空做缓慢自转,形成空间流动感。

二、HTML/CSS & 库加载

  • body { overflow: hidden; background:black } 全屏 WebGL 背景。
  • 右上角是 <audio> 播放器。
  • 通过 CDN 引入: three@0.140.0/build/three.min.js three@0.140.0/examples/js/controls/OrbitControls.js 这两者匹配“旧式全局 THREE”写法。

三、场景基础

scene / camera / renderer 标准三件套:

  • 透视相机 FOV 75,near=0.1 / far=2000,Z=100。

  • 抗锯齿渲染器,填满窗口。
    -(可优化)建议:renderer.setPixelRatio(window.devicePixelRatio) 让高 DPI 更清晰(性能充裕时)。

  • 轨道控制器: const controls = new THREE.OrbitControls(camera, renderer.domElement); 允许鼠标旋转/缩放观察。
    想要“丝滑阻尼”,可: controls.enableDamping = true; // 并在动画循环里加 controls.update();

四、自定义 Shader“发光体”

  • 顶点着色器:把法线变换到视图空间,传给片元: vNormal = normalize(normalMatrix * normal);
  • 片元着色器:根据与视线方向(z 轴)夹角计算强度: float intensity = pow(0.6 - dot(vNormal, vec3(0.0,0.0,1.0)), 2.0); gl_FragColor = vec4(0.0, 1.0, 1.0, 1.0) * intensity; 视觉效果:面向镜头的区域更亮,形成“边缘辉光/自发光”的感觉。
  • 材质参数: blending: THREE.AdditiveBlending, side: THREE.FrontSide, transparent: true 使用加色混合以叠加高亮。
    改进建议:加色+透明一般配 depthWrite:false 避免透明深度写入带来的排序伪影: depthWrite: false
  • 几何体:IcosahedronGeometry(2, 1)(二十面体细分一级)。
    批量实例:创建 200 个网格,随机分布在 [-200,200]³(因乘 400 再减半)。
    性能评估:每个约百来个三角形,200 个共 ~几万三角,WebGL 轻松应付;共享同一个 ShaderMaterial,节省材质开销。
    (更进一步)可用 InstancedMesh 把 200 次 draw call 合并为 1 次,但要改为实例化方案。

五、星空粒子

  • BufferGeometry + Float32BufferAttribute 存 5000 个随机顶点。
  • PointsMaterial({ color: 0xffffff, size: 0.7 }) 形成星点。
    (可选)可以加 sizeAttenuation:true(默认就是 true),基于透视缩放更自然;或改用带纹理的点精灵实现更“星星”的感觉。

六、灯光

  • 有环境光和跟随相机的点光,但

    当前两类物体都“几乎不吃光”

    • ShaderMaterial 未开启 lights,着色完全自定义,不受灯光影响;
    • PointsMaterial 也是“自发光色”,不受灯光影响。
  • 因此这两盏灯“视觉贡献≈0”,可留作以后加其他受光物体时使用,也可以删掉减一点场景状态切换。

七、点击“爆炸”交互(Raycaster)

  • 鼠标点击 → 归一化设备坐标 → raycaster.setFromCamera()intersectObjects(glowGroup.children)
  • 若命中,随机向量把该网格位置抖走 50~100 单位。
    (可选)可改成给它一个速度,在 animate 中逐帧衰减,效果会更“物理”。

八、音频与可视化

  • 页面上有 <audio id="audio" controls autoplay loop>,同时 Three.js 里又:

    1. 创建 AudioListener 并挂相机;
    2. AudioLoader.load(audio.src, ...) 再次下载同一路径音频,塞进 THREE.Audioplay()
    3. AudioAnalyser(sound, 32) 获取频域数据均值 getAverageFrequency(),驱动缩放。
  • 潜在问题与改进

    1. 重复播放/重复下载
      页面 <audio> 播放一次、AudioLoader 又播一次,音频可能重叠。
      ➜ 选一种即可。最简方案:复用 <audio> 元素作音源const sound = new THREE.Audio(listener); sound.setMediaElementSource(audioElement); // 直接用 <audio> 的流 const analyser = new THREE.AudioAnalyser(sound, 32); 这样不再重复下载,播放器的播放/暂停也直接影响可视化。

    2. 自动播放策略

      现代浏览器通常禁止带声音的自动播放 。

      • 你虽然写了 autoplaysound.play(),但往往会被拦下,除非用户先有手势(点击等)。
      • 兼容做法:在第一次 pointerdown/click 时执行: const ac = listener.context; if (ac.state === 'suspended') ac.resume(); audioElement.play().catch(()=>{ /* 显示提示或忽略 */ });
    3. 跨域 (CORS)
      若用 AudioLoader加远程 MP3,需要服务器响应 Access-Control-Allow-Origin:*,否则 WebAudio 可能拿不到频谱数据。

      • 复用 <audio crossorigin="anonymous"> + setMediaElementSource 可以更稳。
    4. FFT 分辨率
      new THREE.AudioAnalyser(sound, 32) 频段较少,变化较“钝”。

      • 想要更丰富的律动,可用 128/256,再用 getFrequencyData() 做更细粒度的驱动。
  • 动画里用: const data = analyser.getAverageFrequency(); // 0~255 const scale = 1.5 + Math.sin(time + i) * 0.3 + data / 256; 叠加了“个体相位差的正弦摆动 + 音量项”,既保留群体呼吸感又随音乐起伏。

九、动画循环与窗口自适应

  • requestAnimationFrame(animate) 驱动渲染;群组与星空各自缓慢自转。
  • 监听 resize 更新相机投影与渲染尺寸,属于标准写法。
    (可优化)把 const t = performance.now()*0.001; 放循环开头,少做一次 Date.now()
    若启用 enableDamping,记得每帧 controls.update()

十、数值与视觉小建议

  • Shader 里这句: float intensity = pow(0.6 - dot(vNormal, vec3(0,0,1)), 2.0);dot(...) > 0.6 时底数为负,指数是 2.0(整数),在 GLSL 里通常仍能得到正值,但不同平台精度可能不一致。
    更稳:夹取到非负区间: float intensity = pow(max(0.0, 0.6 - dot(vNormal, vec3(0,0,1))), 2.0);
  • 透明加色材质建议: const shaderMaterial = new THREE.ShaderMaterial({ vertexShader, fragmentShader, blending: THREE.AdditiveBlending, transparent: true, side: THREE.FrontSide, depthWrite: false // ★ 推荐 });
  • 画质/性能开关: renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); 在 4K 屏上能避免过高像素负担。

十一、一步到位的音频改造示例(可直接替换你原来的音频段)

目的:不重复下载,不触发自动播放拦截时的黑屏“无响应”,并让频谱与播放器同步。

<audio id="audio" crossorigin="anonymous"
       src="https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3"
       controls loop></audio>
// 音频(替换原有 AudioLoader 部分)
const audioEl = document.getElementById('audio');
const listener = new THREE.AudioListener();
camera.add(listener);

const sound = new THREE.Audio(listener);
sound.setMediaElementSource(audioEl); // 直接复用 <audio> 元素
const analyser = new THREE.AudioAnalyser(sound, 128);

// 解决自动播放限制:用户首次点击页面时恢复 AudioContext 并尝试播放
let audioInit = false;
function initAudioOnce() {
  if (audioInit) return;
  audioInit = true;
  const ctx = listener.context;
  if (ctx.state === 'suspended') ctx.resume();
  audioEl.play().catch(() => {/* 可以提示“请点击播放” */});
}
window.addEventListener('pointerdown', initAudioOnce, { once: true });

你可以直接用复制开头的代码到记事本并另存为.html格式然后在浏览器里跑,实现效果:
在这里插入图片描述
该代码可通过鼠标进行交互。


最后推荐一个超酷的ThreeJS网站:https://ykob.github.io/sketch-threejs/
在这里插入图片描述

重拾编程的乐趣和无尽的探索欲在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到