生成 Three.js 双摆物理系统
2025-12-19
生成 Three.js 双摆物理系统
思考过程
**Modeling the Pendulum**
I'm currently focused on the double pendulum system, aiming for a physics-based animation. I'm leaning toward using Lagrangian mechanics, but Newton-Euler is still under consideration. My initial parameters will be the angles $\theta_1$ and $\theta_2$ to specify the coordinates. The plan is to visualize this in 3D using Three.js.
**Considering Implementation Details**
I've been mapping out the specifics of the Three.js integration, focusing on visual elements like spheres for pivot points and masses, and cylinders or lines for rods. Implementing the trail effect using buffer geometry is also on the to-do list. The physics equations, specifically the Runge-Kutta 4 implementation, are next.
**Structuring the Visualization**
Now I'm diving deeper into the Three.js implementation, I'm thinking about the coordinate transformations needed for the pendulum's motion, going from angle and length parameters to the x,y,z coordinates in the 3D space. The visual elements are becoming more defined, using lines for rods, spheres for masses, and a trail effect using buffer geometry to trace the path. Next, I plan to focus on implementing the physics and syncing everything together.
在 Three.js 中实现双摆(Double Pendulum)物理系统,主要分为两个部分:**物理模拟(数学计算)**和**3D 渲染(Three.js)**。
双摆是一个经典的混沌系统,其运动方程通常使用拉格朗日力学导出。
### 核心实现思路
1. **物理模型**:使用 Runge-Kutta (RK4) 数值积分法求解二阶微分方程,比简单的欧拉法更稳定。
2. **可视化**:使用 `SphereGeometry` 表示摆锤,`BufferGeometry` 实时更新连线,并添加轨迹(Trail)效果。
### 完整代码示例
你可以将以下代码保存为 `.html` 文件并用浏览器打开。
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Three.js 双摆物理模拟</title>
<style>
body { margin: 0; overflow: hidden; background-color: #000; }
canvas { display: block; }
#info {
position: absolute; top: 10px; left: 10px; color: white;
font-family: sans-serif; pointer-events: none;
}
</style>
</head>
<body>
<div id="info">Three.js 双摆系统 (RK4 积分器)</div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// --- 物理参数 ---
const g = 9.81; // 重力加速度
const m1 = 10; // 摆锤1质量
const m2 = 10; // 摆锤2质量
const l1 = 2; // 摆臂1长度
const l2 = 2; // 摆臂2长度
// 初始状态 [theta1, omega1, theta2, omega2]
let state = [Math.PI / 2, 0, Math.PI / 2, 0];
// --- Three.js 初始化 ---
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0, 8);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
// 灯光
const light = new THREE.PointLight(0xffffff, 100);
light.position.set(5, 5, 5);
scene.add(light);
scene.add(new THREE.AmbientLight(0x404040));
// --- 摆动组件 ---
const material = new THREE.MeshStandardMaterial({ color: 0x00ff88 });
// 摆锤
const sphere1 = new THREE.Mesh(new THREE.SphereGeometry(0.2, 32, 32), material);
const sphere2 = new THREE.Mesh(new THREE.SphereGeometry(0.2, 32, 32), material);
scene.add(sphere1);
scene.add(sphere2);
// 摆杆
const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });
const lineGeometry = new THREE.BufferGeometry();
const line = new THREE.Line(lineGeometry, lineMaterial);
scene.add(line);
// 轨迹
const MAX_TRAIL_POINTS = 500;
const trailPositions = [];
const trailGeometry = new THREE.BufferGeometry();
const trailMaterial = new THREE.LineBasicMaterial({ color: 0xff0000, transparent: true, opacity: 0.6 });
const trail = new THREE.Line(trailGeometry, trailMaterial);
scene.add(trail);
// --- 物理引擎 (RK4) ---
function derivatives(s) {
const [t1, w1, t2, w2] = s;
// 计算 theta1 的角加速度
let num1 = -g * (2 * m1 + m2) * Math.sin(t1);
let num2 = -m2 * g * Math.sin(t1 - 2 * t2);
let num3 = -2 * Math.sin(t1 - t2) * m2;
let num4 = w2 * w2 * l2 + w1 * w1 * l1 * Math.cos(t1 - t2);
let den = l1 * (2 * m1 + m2 - m2 * Math.cos(2 * t1 - 2 * t2));
const dw1 = (num1 + num2 + num3 * num4) / den;
// 计算 theta2 的角加速度
num1 = 2 * Math.sin(t1 - t2);
num2 = (w1 * w1 * l1 * (m1 + m2));
num3 = g * (m1 + m2) * Math.cos(t1);
num4 = w2 * w2 * l2 * m2 * Math.cos(t1 - t2);
den = l2 * (2 * m1 + m2 - m2 * Math.cos(2 * t1 - 2 * t2));
const dw2 = (num1 * (num2 + num3 + num4)) / den;
return [w1, dw1, w2, dw2];
}
function updatePhysics(dt) {
// Runge-Kutta 4th Order Step
const k1 = derivatives(state);
const k2 = derivatives(state.map((v, i) => v + k1[i] * dt / 2));
const k3 = derivatives(state.map((v, i) => v + k2[i] * dt / 2));
const k4 = derivatives(state.map((v, i) => v + k3[i] * dt));
state = state.map((v, i) => v + (dt / 6) * (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i]));
}
// --- 动画循环 ---
function animate() {
requestAnimationFrame(animate);
// 物理步进 (为了平滑,每帧运行多次小步进)
const subSteps = 5;
const dt = 0.016 / subSteps;
for(let i=0; i<subSteps; i++) updatePhysics(dt);
const [t1, w1, t2, w2] = state;
// 转换为笛卡尔坐标
const x1 = l1 * Math.sin(t1);
const y1 = -l1 * Math.cos(t1);
const x2 = x1 + l2 * Math.sin(t2);
const y2 = y1 - l2 * Math.cos(t2);
// 更新模型位置
sphere1.position.set(x1, y1, 0);
sphere2.position.set(x2, y2, 0);
// 更新摆杆
lineGeometry.setFromPoints([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(x1, y1, 0),
new THREE.Vector3(x2, y2, 0)
]);
// 更新轨迹
trailPositions.push(new THREE.Vector3(x2, y2, 0));
if (trailPositions.length > MAX_TRAIL_POINTS) trailPositions.shift();
trailGeometry.setFromPoints(trailPositions);
renderer.render(scene, camera);
}
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
animate();
</script>
</body>
</html>
```
### 关键部分解析:
1. **物理公式 (The Math)**:
双摆的角加速度公式非常复杂,代码中的 `derivatives` 函数实现了这些非线性方程。公式考虑了重力、长度、质量以及两节摆臂之间的相互作用。
2. **数值积分 (RK4)**:
如果简单的使用 `angle += velocity * dt`,系统会迅速因为舍入误差积累能量而“爆炸”(摆动幅度越来越大,不守恒)。**RK4(四阶龙格-库塔法)** 是一种标准的高精度数值方法,能够保持物理模拟的长期稳定性。
3. **坐标转换**:
物理计算是在极坐标系(角度 $\theta$)下进行的,而 Three.js 使用的是笛卡尔坐标系($x, y, z$)。
* $x_1 = L_1 \sin(\theta_1)$
* $y_1 = -L_1 \cos(\theta_1)$
* $x_2 = x_1 + L_2 \sin(\theta_2)$
* $y_2 = y_1 - L_2 \cos(\theta_2)$
4. **轨迹效果 (Trail)**:
使用一个数组 `trailPositions` 记录摆锤2的历史位置,并通过 `trailGeometry.setFromPoints` 实时更新。这能直观地展示双摆著名的“混沌”路径。
5. **性能优化**:
在 `animate` 函数中,我使用了 `subSteps`。这是因为物理引擎需要较小的时间步长(dt)来保持精确,而屏幕刷新率通常只有 60fps。通过每帧进行多次小的物理步进,可以获得更稳定的模拟效果。
### 如何扩展?
* **交互性**:可以使用 `dat.gui` 插件实时调整 $g$(重力)、$m$(质量)或摆长。
* **3D 化**:可以给摆锤换上更复杂的模型(比如金属材质),或者添加背景环境贴图(CubeMap)。
* **多重摆**:你可以尝试将数组扩展,模拟三摆或多摆系统,虽然公式会呈几何倍数变复杂。