今天写一个超级酷炫的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, // 改为 FrontSidetransparent: 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
。 - 场景里有两类物体:
- 200 个“发光”小多面体(用自定义 Shader 做到类似辉光的视觉)
- 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
也是“自发光色”,不受灯光影响。
- ShaderMaterial 未开启
-
因此这两盏灯“视觉贡献≈0”,可留作以后加其他受光物体时使用,也可以删掉减一点场景状态切换。
七、点击“爆炸”交互(Raycaster)
- 鼠标点击 → 归一化设备坐标 →
raycaster.setFromCamera()
→intersectObjects(glowGroup.children)
。 - 若命中,随机向量把该网格位置抖走 50~100 单位。
(可选)可改成给它一个速度,在animate
中逐帧衰减,效果会更“物理”。
八、音频与可视化
-
页面上有
<audio id="audio" controls autoplay loop>
,同时 Three.js 里又:- 创建
AudioListener
并挂相机; - 用
AudioLoader.load(audio.src, ...)
再次下载同一路径音频,塞进THREE.Audio
并play()
; - 用
AudioAnalyser(sound, 32)
获取频域数据均值getAverageFrequency()
,驱动缩放。
- 创建
-
潜在问题与改进
-
重复播放/重复下载
页面<audio>
播放一次、AudioLoader
又播一次,音频可能重叠。
➜ 选一种即可。最简方案:复用<audio>
元素作音源:const sound = new THREE.Audio(listener); sound.setMediaElementSource(audioElement); // 直接用 <audio> 的流 const analyser = new THREE.AudioAnalyser(sound, 32);
这样不再重复下载,播放器的播放/暂停也直接影响可视化。 -
自动播放策略
现代浏览器通常禁止带声音的自动播放 。
- 你虽然写了
autoplay
和sound.play()
,但往往会被拦下,除非用户先有手势(点击等)。 - 兼容做法:在第一次
pointerdown/click
时执行:const ac = listener.context; if (ac.state === 'suspended') ac.resume(); audioElement.play().catch(()=>{ /* 显示提示或忽略 */ });
- 你虽然写了
-
跨域 (CORS)
若用AudioLoader
加远程 MP3,需要服务器响应Access-Control-Allow-Origin:*
,否则 WebAudio 可能拿不到频谱数据。- 复用
<audio crossorigin="anonymous">
+setMediaElementSource
可以更稳。
- 复用
-
FFT 分辨率
new THREE.AudioAnalyser(sound, 32)
频段较少,变化较“钝”。- 想要更丰富的律动,可用 128/256,再用
getFrequencyData()
做更细粒度的驱动。
- 想要更丰富的律动,可用 128/256,再用
-
-
动画里用:
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/
重拾编程的乐趣和无尽的探索欲