今天寫一個超級酷炫的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]3(因乘 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/
重拾編程的樂趣和無盡的探索欲