300 lines
11 KiB
HTML
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>
|