Files
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

300 lines
11 KiB
HTML

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>03 - Sphere Bloom | 斐波那契球面 + 发光效果</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #000;
overflow: hidden;
font-family: 'Space Mono', monospace;
}
#canvas-container {
width: 100vw;
height: 100vh;
}
.info {
position: fixed;
top: 20px;
left: 20px;
color: #00ff88;
font-size: 14px;
pointer-events: none;
z-index: 100;
text-shadow: 0 0 10px rgba(0,255,136,0.5);
}
.info h1 { font-size: 20px; margin-bottom: 8px; }
.info p { opacity: 0.8; font-size: 12px; margin: 4px 0; }
.controls {
position: fixed;
bottom: 20px;
left: 20px;
color: #fff;
font-size: 12px;
background: rgba(0,0,0,0.7);
padding: 15px;
border-radius: 8px;
border: 1px solid #333;
backdrop-filter: blur(10px);
}
.control-group {
margin: 10px 0;
display: flex;
align-items: center;
gap: 10px;
}
.control-group label {
width: 100px;
color: #aaa;
}
.control-group input[type="range"] {
width: 150px;
}
.control-group span {
color: #00ff88;
min-width: 50px;
}
.toggle {
display: flex;
align-items: center;
gap: 10px;
margin: 10px 0;
}
.toggle label {
color: #aaa;
}
.toggle input[type="checkbox"] {
width: 20px;
height: 20px;
accent-color: #00ff88;
}
</style>
</head>
<body>
<div id="canvas-container"></div>
<div class="info">
<h1>03 - Sphere Bloom</h1>
<p>斐波那契球面分布 + 后期发光</p>
<p>粒子数: <span id="count">0</span></p>
<p>FPS: <span id="fps">0</span></p>
</div>
<div class="controls">
<div class="control-group">
<label>发光强度</label>
<input type="range" id="bloomStrength" min="0" max="3" step="0.1" value="1.5">
<span id="bloomVal">1.5</span>
</div>
<div class="control-group">
<label>发光半径</label>
<input type="range" id="bloomRadius" min="0" max="1" step="0.05" value="0.4">
<span id="radiusVal">0.4</span>
</div>
<div class="control-group">
<label>球体半径</label>
<input type="range" id="sphereRadius" min="10" max="50" step="1" value="30">
<span id="sphereVal">30</span>
</div>
<div class="toggle">
<label>自动旋转</label>
<input type="checkbox" id="autoRotate" checked>
</div>
</div>
<!-- Three.js Core -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<!-- Three.js Examples (Controls & Post-processing) -->
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/EffectComposer.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/RenderPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/ShaderPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/CopyShader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/LuminosityHighPassShader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/UnrealBloomPass.js"></script>
<script>
// CONFIG
const COUNT = 15000; // 15000 个粒子,平衡性能和效果
const AUTO_SPIN = true;
// SCENE SETUP
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x000000, 0.008);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 2000);
camera.position.set(0, 0, 100);
const renderer = new THREE.WebGLRenderer({
antialias: true,
powerPreference: "high-performance"
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ReinhardToneMapping;
document.getElementById('canvas-container').appendChild(renderer.domElement);
// ORBIT CONTROLS
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.autoRotate = AUTO_SPIN;
controls.autoRotateSpeed = 2.0;
// POST PROCESSING - BLOOM
const renderScene = new THREE.RenderPass(scene, camera);
const bloomPass = new THREE.UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.5, // strength
0.4, // radius
0 // threshold
);
bloomPass.strength = 1.5;
bloomPass.radius = 0.4;
bloomPass.threshold = 0;
const composer = new THREE.EffectComposer(renderer);
composer.addPass(renderScene);
composer.addPass(bloomPass);
// INSTANCED MESH - 高性能实例渲染
const dummy = new THREE.Object3D();
const color = new THREE.Color();
const target = new THREE.Vector3();
// 使用四面体作为粒子形状(比点更有立体感)
const geometry = new THREE.TetrahedronGeometry(0.2, 0);
const material = new THREE.MeshBasicMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.9
});
const instancedMesh = new THREE.InstancedMesh(geometry, material, COUNT);
instancedMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
scene.add(instancedMesh);
// 存储每个粒子的当前位置
const positions = [];
for (let i = 0; i < COUNT; i++) {
positions.push(new THREE.Vector3(
(Math.random() - 0.5) * 100,
(Math.random() - 0.5) * 100,
(Math.random() - 0.5) * 100
));
// 初始颜色:青绿色
instancedMesh.setColorAt(i, color.setHex(0x00ff88));
}
// UI CONTROLS
const bloomStrengthSlider = document.getElementById('bloomStrength');
const bloomRadiusSlider = document.getElementById('bloomRadius');
const sphereRadiusSlider = document.getElementById('sphereRadius');
const autoRotateCheck = document.getElementById('autoRotate');
bloomStrengthSlider.addEventListener('input', (e) => {
bloomPass.strength = parseFloat(e.target.value);
document.getElementById('bloomVal').textContent = e.target.value;
});
bloomRadiusSlider.addEventListener('input', (e) => {
bloomPass.radius = parseFloat(e.target.value);
document.getElementById('radiusVal').textContent = e.target.value;
});
sphereRadiusSlider.addEventListener('input', (e) => {
document.getElementById('sphereVal').textContent = e.target.value;
});
autoRotateCheck.addEventListener('change', (e) => {
controls.autoRotate = e.target.checked;
});
// 更新显示
document.getElementById('count').textContent = COUNT;
// FPS 计算
let lastTime = performance.now();
let frameCount = 0;
const fpsEl = document.getElementById('fps');
// ANIMATION LOOP
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
// FPS 计算
const now = performance.now();
frameCount++;
if (now - lastTime >= 1000) {
fpsEl.textContent = frameCount;
frameCount = 0;
lastTime = now;
}
const time = clock.getElapsedTime();
const count = COUNT;
const r = parseFloat(sphereRadiusSlider.value);
// 核心算法:斐波那契球面分布
for (let i = 0; i < COUNT; i++) {
// 黄金角 ~2.39996322972865332
const phi = Math.acos(-1 + (2 * i) / count);
const theta = Math.sqrt(count * Math.PI) * phi;
// 计算目标位置(球面坐标转笛卡尔坐标)
target.set(
r * Math.cos(theta) * Math.sin(phi),
r * Math.sin(theta) * Math.sin(phi),
r * Math.cos(phi)
);
// 添加轻微的波动动画
const wave = Math.sin(time * 2 + i * 0.01) * 2;
target.addScalar(wave * 0.1);
// 平滑插值移动
positions[i].lerp(target, 0.05);
// 更新实例位置和矩阵
dummy.position.copy(positions[i]);
// 让粒子面向相机(billboard 效果)
dummy.lookAt(camera.position);
dummy.updateMatrix();
instancedMesh.setMatrixAt(i, dummy.matrix);
// 动态颜色:基于高度
const hue = 0.35 + (positions[i].y / r) * 0.15; // 青到绿
const saturation = 0.8;
const lightness = 0.5 + Math.sin(time * 3 + i * 0.001) * 0.1;
color.setHSL(hue, saturation, lightness);
instancedMesh.setColorAt(i, color);
}
instancedMesh.instanceMatrix.needsUpdate = true;
instancedMesh.instanceColor.needsUpdate = true;
controls.update();
// 使用 composer 渲染(包含 bloom 效果)
composer.render();
}
animate();
// 响应窗口调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>