我们将创建一个具有物理模拟效果的粒子系统,包括重力、碰撞和交互功能。这个可视化效果将展示粒子在3D空间中的物理行为。
创建基础Three.js场景,包含相机、灯光和渲染器
实现粒子系统,使用自定义几何体或BufferGeometry
添加物理模拟(重力、碰撞检测、速度/加速度)
实现用户交互(鼠标拖动、点击生成粒子)
添加控制面板以调整物理参数
添加视觉反馈和性能显示
下面是完整实现代码:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js 粒子物理模拟</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lil-gui@0.17.0/dist/lil-gui.umd.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #0a0a1a;
color: #e0e0ff;
overflow: hidden;
height: 100vh;
}
#container {
position: relative;
width: 100%;
height: 100vh;
}
#info {
position: absolute;
top: 20px;
left: 20px;
z-index: 100;
background: rgba(10, 10, 30, 0.7);
padding: 15px;
border-radius: 10px;
max-width: 300px;
backdrop-filter: blur(5px);
border: 1px solid rgba(100, 150, 255, 0.3);
}
h1 {
font-size: 24px;
margin-bottom: 10px;
background: linear-gradient(90deg, #4facfe, #00f2fe);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
font-size: 14px;
margin-bottom: 15px;
color: #a0a0ff;
}
#stats {
display: flex;
justify-content: space-between;
margin-top: 10px;
font-size: 14px;
}
.stat-value {
color: #00f2fe;
font-weight: bold;
}
#controls {
position: absolute;
top: 20px;
right: 20px;
z-index: 100;
}
#instructions {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
text-align: center;
background: rgba(10, 10, 30, 0.7);
padding: 10px 15px;
border-radius: 10px;
font-size: 14px;
backdrop-filter: blur(5px);
border: 1px solid rgba(100, 150, 255, 0.3);
}
canvas {
display: block;
}
</style>
</head>
<body>
<div id="container"></div>
<div id="info">
<h1>粒子物理模拟</h1>
<p class="subtitle">Three.js粒子系统与物理引擎模拟</p>
<p>体验粒子在重力、碰撞和相互作用下的行为。</p>
<div id="stats">
<div>粒子数量: <span class="stat-value" id="particleCount">0</span></div>
<div>帧率: <span class="stat-value" id="fps">0</span> FPS</div>
<div>物理计算: <span class="stat-value" id="physicsTime">0</span> ms</div>
</div>
</div>
<div id="instructions">
点击或拖动鼠标生成粒子 | 右键拖拽旋转视角 | 滚轮缩放
</div>
<script>
// 全局变量
let scene, camera, renderer, particles;
let mouse = { x: 0, y: 0, isDown: false, isRightDown: false };
let particleCount = 0;
const maxParticles = 5000;
let clock = new THREE.Clock();
// 物理参数
const physics = {
gravity: 0.5,
bounce: 0.8,
friction: 0.99,
attractionForce: 0.5,
repulsionForce: 2.0,
collisionDistance: 1.5,
mouseInfluence: 10.0,
timeScale: 1.0
};
// 粒子属性
let particlePositions = [];
let particleVelocities = [];
let particleColors = [];
let particleSizes = [];
let particleGeometry, particleMaterial, particleSystem;
// 初始化场景
function init() {
// 创建场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a1a);
// 创建相机
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0, 50);
// 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.getElementById('container').appendChild(renderer.domElement);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404080, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 20, 15);
scene.add(directionalLight);
// 创建粒子系统
createParticleSystem();
// 添加初始粒子
for (let i = 0; i < 500; i++) {
addParticle(
(Math.random() - 0.5) * 40,
(Math.random() - 0.5) * 40,
(Math.random() - 0.5) * 40
);
}
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.screenSpacePanning = false;
controls.minDistance = 10;
controls.maxDistance = 100;
controls.maxPolarAngle = Math.PI / 2;
// 添加GUI控制面板
createGUI();
// 事件监听
setupEventListeners();
// 开始动画
animate();
}
// 创建粒子系统
function createParticleSystem() {
// 创建粒子几何体
particleGeometry = new THREE.BufferGeometry();
// 初始化粒子数据
const positions = new Float32Array(maxParticles * 3);
const colors = new Float32Array(maxParticles * 3);
const sizes = new Float32Array(maxParticles);
particleGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
particleGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
particleGeometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
// 创建粒子材质
particleMaterial = new THREE.PointsMaterial({
size: 0.5,
vertexColors: true,
transparent: true,
opacity: 0.8,
sizeAttenuation: true
});
// 创建粒子系统
particleSystem = new THREE.Points(particleGeometry, particleMaterial);
scene.add(particleSystem);
// 初始化粒子数组
particlePositions = new Float32Array(maxParticles * 3);
particleVelocities = new Array(maxParticles).fill().map(() => new THREE.Vector3());
particleColors = new Float32Array(maxParticles * 3);
particleSizes = new Float32Array(maxParticles);
}
// 添加一个粒子
function addParticle(x, y, z) {
if (particleCount >= maxParticles) return;
const index = particleCount * 3;
// 设置位置
particlePositions[index] = x;
particlePositions[index + 1] = y;
particlePositions[index + 2] = z;
// 设置随机速度
particleVelocities[particleCount].set(
(Math.random() - 0.5) * 2,
(Math.random() - 0.5) * 2,
(Math.random() - 0.5) * 2
);
// 设置随机颜色
const color = new THREE.Color();
color.setHSL(Math.random() * 0.2 + 0.5, 0.8, Math.random() * 0.3 + 0.5);
particleColors[index] = color.r;
particleColors[index + 1] = color.g;
particleColors[index + 2] = color.b;
// 设置随机大小
particleSizes[particleCount] = Math.random() * 0.5 + 0.3;
particleCount++;
// 更新几何体属性
updateParticleAttributes();
}
// 更新粒子属性
function updateParticleAttributes() {
particleGeometry.attributes.position.array.set(particlePositions);
particleGeometry.attributes.color.array.set(particleColors);
particleGeometry.attributes.size.array.set(particleSizes);
particleGeometry.attributes.position.needsUpdate = true;
particleGeometry.attributes.color.needsUpdate = true;
particleGeometry.attributes.size.needsUpdate = true;
// 更新统计信息
document.getElementById('particleCount').textContent = particleCount;
}
// 物理模拟
function simulatePhysics(deltaTime) {
const startTime = performance.now();
// 应用时间缩放
deltaTime *= physics.timeScale;
// 边界盒
const boundary = 25;
// 更新每个粒子
for (let i = 0; i < particleCount; i++) {
const index = i * 3;
// 应用重力
particleVelocities[i].y -= physics.gravity * deltaTime;
// 应用摩擦力
particleVelocities[i].multiplyScalar(physics.friction);
// 更新位置
particlePositions[index] += particleVelocities[i].x * deltaTime;
particlePositions[index + 1] += particleVelocities[i].y * deltaTime;
particlePositions[index + 2] += particleVelocities[i].z * deltaTime;
// 边界碰撞检测
if (particlePositions[index] < -boundary) {
particlePositions[index] = -boundary;
particleVelocities[i].x = -particleVelocities[i].x * physics.bounce;
} else if (particlePositions[index] > boundary) {
particlePositions[index] = boundary;
particleVelocities[i].x = -particleVelocities[i].x * physics.bounce;
}
if (particlePositions[index + 1] < -boundary) {
particlePositions[index + 1] = -boundary;
particleVelocities[i].y = -particleVelocities[i].y * physics.bounce;
} else if (particlePositions[index + 1] > boundary) {
particlePositions[index + 1] = boundary;
particleVelocities[i].y = -particleVelocities[i].y * physics.bounce;
}
if (particlePositions[index + 2] < -boundary) {
particlePositions[index + 2] = -boundary;
particleVelocities[i].z = -particleVelocities[i].z * physics.bounce;
} else if (particlePositions[index + 2] > boundary) {
particlePositions[index + 2] = boundary;
particleVelocities[i].z = -particleVelocities[i].z * physics.bounce;
}
// 粒子间相互作用(简化的碰撞检测)
for (let j = i + 1; j < particleCount; j++) {
const jIndex = j * 3;
const dx = particlePositions[index] - particlePositions[jIndex];
const dy = particlePositions[index + 1] - particlePositions[jIndex + 1];
const dz = particlePositions[index + 2] - particlePositions[jIndex + 2];
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (distance < physics.collisionDistance && distance > 0) {
// 简单碰撞响应
const force = (physics.collisionDistance - distance) / physics.collisionDistance;
const forceX = dx / distance * force * physics.repulsionForce * deltaTime;
const forceY = dy / distance * force * physics.repulsionForce * deltaTime;
const forceZ = dz / distance * force * physics.repulsionForce * deltaTime;
particleVelocities[i].x += forceX;
particleVelocities[i].y += forceY;
particleVelocities[i].z += forceZ;
particleVelocities[j].x -= forceX;
particleVelocities[j].y -= forceY;
particleVelocities[j].z -= forceZ;
}
}
// 鼠标交互
if (mouse.isDown) {
// 将鼠标位置转换为3D空间
const mouseVector = new THREE.Vector3(
(mouse.x / window.innerWidth) * 2 - 1,
-(mouse.y / window.innerHeight) * 2 + 1,
0.5
);
mouseVector.unproject(camera);
mouseVector.sub(camera.position).normalize();
const distance = -camera.position.z / mouseVector.z;
const mousePos = camera.position.clone().add(mouseVector.multiplyScalar(distance));
const dx = mousePos.x - particlePositions[index];
const dy = mousePos.y - particlePositions[index + 1];
const dz = mousePos.z - particlePositions[index + 2];
const distanceToMouse = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (distanceToMouse < 10) {
const force = (10 - distanceToMouse) / 10 * physics.mouseInfluence * deltaTime;
particleVelocities[i].x += dx / distanceToMouse * force;
particleVelocities[i].y += dy / distanceToMouse * force;
particleVelocities[i].z += dz / distanceToMouse * force;
}
}
}
// 更新物理计算时间统计
const physicsTime = performance.now() - startTime;
document.getElementById('physicsTime').textContent = physicsTime.toFixed(2);
}
// 创建GUI控制面板
function createGUI() {
const gui = new lil.GUI({ autoPlace: false });
document.getElementById('container').appendChild(gui.domElement);
gui.domElement.style.position = 'absolute';
gui.domElement.style.top = '20px';
gui.domElement.style.right = '20px';
// 物理参数控制
const physicsFolder = gui.addFolder('物理参数');
physicsFolder.add(physics, 'gravity', 0, 2, 0.1).name('重力');
physicsFolder.add(physics, 'bounce', 0, 1, 0.05).name('弹性');
physicsFolder.add(physics, 'friction', 0.9, 1, 0.001).name('摩擦力');
physicsFolder.add(physics, 'repulsionForce', 0, 5, 0.1).name('排斥力');
physicsFolder.add(physics, 'collisionDistance', 0.5, 3, 0.1).name('碰撞距离');
physicsFolder.add(physics, 'mouseInfluence', 0, 20, 0.5).name('鼠标影响');
physicsFolder.add(physics, 'timeScale', 0.1, 3, 0.1).name('时间缩放');
physicsFolder.open();
// 粒子控制
const particleFolder = gui.addFolder('粒子控制');
particleFolder.add({ addParticles: () => {
for (let i = 0; i < 50; i++) {
addParticle(
(Math.random() - 0.5) * 20,
(Math.random() - 0.5) * 20,
(Math.random() - 0.5) * 20
);
}
} }, 'addParticles').name('添加粒子');
particleFolder.add({ removeParticles: () => {
if (particleCount > 0) {
particleCount = Math.max(0, particleCount - 100);
updateParticleAttributes();
}
} }, 'removeParticles').name('移除粒子');
particleFolder.add({ resetParticles: () => {
particleCount = 0;
for (let i = 0; i < 500; i++) {
addParticle(
(Math.random() - 0.5) * 40,
(Math.random() - 0.5) * 40,
(Math.random() - 0.5) * 40
);
}
} }, 'resetParticles').name('重置粒子');
particleFolder.open();
// 视觉控制
const visualFolder = gui.addFolder('视觉设置');
visualFolder.add(particleMaterial, 'size', 0.1, 2, 0.1).name('粒子大小');
visualFolder.add(particleMaterial, 'opacity', 0.1, 1, 0.05).name('透明度');
visualFolder.open();
}
// 设置事件监听器
function setupEventListeners() {
// 鼠标移动事件
renderer.domElement.addEventListener('mousemove', (event) => {
mouse.x = event.clientX;
mouse.y = event.clientY;
});
// 鼠标按下事件
renderer.domElement.addEventListener('mousedown', (event) => {
if (event.button === 0) { // 左键
mouse.isDown = true;
// 添加粒子
const mouseVector = new THREE.Vector3(
(event.clientX / window.innerWidth) * 2 - 1,
-(event.clientY / window.innerHeight) * 2 + 1,
0.5
);
mouseVector.unproject(camera);
mouseVector.sub(camera.position).normalize();
const distance = -camera.position.z / mouseVector.z;
const position = camera.position.clone().add(mouseVector.multiplyScalar(distance));
for (let i = 0; i < 10; i++) {
addParticle(
position.x + (Math.random() - 0.5) * 2,
position.y + (Math.random() - 0.5) * 2,
position.z + (Math.random() - 0.5) * 2
);
}
} else if (event.button === 2) { // 右键
mouse.isRightDown = true;
}
});
// 鼠标释放事件
renderer.domElement.addEventListener('mouseup', (event) => {
if (event.button === 0) {
mouse.isDown = false;
} else if (event.button === 2) {
mouse.isRightDown = false;
}
});
// 鼠标离开画布
renderer.domElement.addEventListener('mouseleave', () => {
mouse.isDown = false;
mouse.isRightDown = false;
});
// 阻止右键菜单
renderer.domElement.addEventListener('contextmenu', (event) => {
event.preventDefault();
});
// 窗口调整大小
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
}
// 帧率计算
let frameCount = 0;
let lastTime = performance.now();
let fps = 0;
function updateFPS() {
frameCount++;
const currentTime = performance.now();
if (currentTime >= lastTime + 1000) {
fps = Math.round((frameCount * 1000) / (currentTime - lastTime));
frameCount = 0;
lastTime = currentTime;
document.getElementById('fps').textContent = fps;
}
}
// 动画循环
function animate() {
requestAnimationFrame(animate);
const deltaTime = clock.getDelta();
// 物理模拟
simulatePhysics(deltaTime);
// 更新粒子属性
updateParticleAttributes();
// 更新帧率
updateFPS();
// 渲染场景
renderer.render(scene, camera);
}
// 初始化应用
init();
</script>
</body>
</html>
物理模拟效果:
重力:粒子受重力影响下落
碰撞检测:粒子与边界及其他粒子碰撞
弹性:碰撞后粒子会反弹
摩擦力:粒子运动逐渐减缓
交互功能:
鼠标左键点击生成粒子
鼠标左键拖拽影响粒子运动
鼠标右键拖拽旋转视角
滚轮缩放视角
控制面板:
调整物理参数(重力、弹性、摩擦力等)
控制粒子数量(添加、移除、重置)
调整视觉设置(粒子大小、透明度)
性能监控:
实时显示粒子数量
帧率(FPS)显示
物理计算时间
这个示例展示了Three.js粒子系统与物理模拟的结合,通过调整参数可以观察到不同的粒子行为,适合学习和演示物理模拟原理。