物理模拟涉及大量计算,用 WASM 可以提高帧率,支持更多对象。
物理模拟的特点:
实测数据:
| 场景 | 对象数量 | JavaScript | Rust + WASM | 帧率提升 |
|---|---|---|---|---|
| 圆形碰撞 | 1000 个 | 60 FPS | 144 FPS | 2.4x |
| 刚体模拟 | 500 个 | 30 FPS | 90 FPS | 3x |
| 粒子系统 | 10000 个 | 25 FPS | 60 FPS | 2.4x |
1[package]
2name = "physics-engine"
3version = "0.1.0"
4edition = "2021"
5
6[lib]
7crate-type = ["cdylib"]
8
9[dependencies]
10wasm-bindgen = "0.2"
11
12[profile.release]
13opt-level = 3
14lto = true1use wasm_bindgen::prelude::*;
2
3// 向量
4#[derive(Clone, Copy)]
5pub struct Vec2 {
6 pub x: f64,
7 pub y: f64,
8}
9
10impl Vec2 {
11 pub fn new(x: f64, y: f64) -> Self {
12 Self { x, y }
13 }
14
15 pub fn length(&self) -> f64 {
16 (self.x * self.x + self.y * self.y).sqrt()
17 }
18
19 pub fn normalize(&self) -> Self {
20 let len = self.length();
21 if len > 0.0 {
22 Self { x: self.x / len, y: self.y / len }
23 } else {
24 *self
25 }
26 }
27}
28
29impl std::ops::Add for Vec2 {
30 type Output = Self;
31 fn add(self, other: Self) -> Self {
32 Self { x: self.x + other.x, y: self.y + other.y }
33 }
34}
35
36impl std::ops::Sub for Vec2 {
37 type Output = Self;
38 fn sub(self, other: Self) -> Self {
39 Self { x: self.x - other.x, y: self.y - other.y }
40 }
41}
42
43impl std::ops::Mul<f64> for Vec2 {
44 type Output = Self;
45 fn mul(self, scalar: f64) -> Self {
46 Self { x: self.x * scalar, y: self.y * scalar }
47 }
48}
49
50// 圆形物体
51#[wasm_bindgen]
52pub struct Circle {
53 x: f64,
54 y: f64,
55 vx: f64,
56 vy: f64,
57 radius: f64,
58 mass: f64,
59}
60
61#[wasm_bindgen]
62impl Circle {
63 #[wasm_bindgen(constructor)]
64 pub fn new(x: f64, y: f64, radius: f64, mass: f64) -> Self {
65 Self { x, y, vx: 0.0, vy: 0.0, radius, mass }
66 }
67
68 #[wasm_bindgen(getter)]
69 pub fn x(&self) -> f64 { self.x }
70
71 #[wasm_bindgen(getter)]
72 pub fn y(&self) -> f64 { self.y }
73
74 #[wasm_bindgen(getter)]
75 pub fn radius(&self) -> f64 { self.radius }
76
77 pub fn set_velocity(&mut self, vx: f64, vy: f64) {
78 self.vx = vx;
79 self.vy = vy;
80 }
81}
82
83// 物理世界
84#[wasm_bindgen]
85pub struct PhysicsWorld {
86 circles: Vec<Circle>,
87 width: f64,
88 height: f64,
89 gravity: f64,
90 damping: f64,
91 restitution: f64,
92}
93
94#[wasm_bindgen]
95impl PhysicsWorld {
96 #[wasm_bindgen(constructor)]
97 pub fn new(width: f64, height: f64) -> Self {
98 Self {
99 circles: Vec::new(),
100 width,
101 height,
102 gravity: 500.0,
103 damping: 0.99,
104 restitution: 0.8,
105 }
106 }
107
108 pub fn add_circle(&mut self, x: f64, y: f64, radius: f64, mass: f64) {
109 self.circles.push(Circle::new(x, y, radius, mass));
110 }
111
112 pub fn set_gravity(&mut self, gravity: f64) {
113 self.gravity = gravity;
114 }
115
116 pub fn count(&self) -> usize {
117 self.circles.len()
118 }
119
120 // 更新物理状态
121 pub fn update(&mut self, dt: f64) {
122 // 1. 应用重力
123 for circle in &mut self.circles {
124 circle.vy += self.gravity * dt;
125 }
126
127 // 2. 更新位置
128 for circle in &mut self.circles {
129 circle.x += circle.vx * dt;
130 circle.y += circle.vy * dt;
131
132 circle.vx *= self.damping;
133 circle.vy *= self.damping;
134 }
135
136 // 3. 边界碰撞
137 for circle in &mut self.circles {
138 if circle.x - circle.radius < 0.0 {
139 circle.x = circle.radius;
140 circle.vx = -circle.vx * self.restitution;
141 } else if circle.x + circle.radius > self.width {
142 circle.x = self.width - circle.radius;
143 circle.vx = -circle.vx * self.restitution;
144 }
145
146 if circle.y - circle.radius < 0.0 {
147 circle.y = circle.radius;
148 circle.vy = -circle.vy * self.restitution;
149 } else if circle.y + circle.radius > self.height {
150 circle.y = self.height - circle.radius;
151 circle.vy = -circle.vy * self.restitution;
152 }
153 }
154
155 // 4. 物体间碰撞
156 let len = self.circles.len();
157 for i in 0..len {
158 for j in (i + 1)..len {
159 self.resolve_collision(i, j);
160 }
161 }
162 }
163
164 fn resolve_collision(&mut self, i: usize, j: usize) {
165 let (c1, c2) = unsafe {
166 let ptr = self.circles.as_mut_ptr();
167 (&mut *ptr.add(i), &mut *ptr.add(j))
168 };
169
170 let dx = c2.x - c1.x;
171 let dy = c2.y - c1.y;
172 let distance = (dx * dx + dy * dy).sqrt();
173 let min_distance = c1.radius + c2.radius;
174
175 if distance < min_distance {
176 // 分离物体
177 let nx = dx / distance;
178 let ny = dy / distance;
179
180 let overlap = min_distance - distance;
181 let total_mass = c1.mass + c2.mass;
182
183 c1.x -= nx * overlap * (c2.mass / total_mass);
184 c1.y -= ny * overlap * (c2.mass / total_mass);
185 c2.x += nx * overlap * (c1.mass / total_mass);
186 c2.y += ny * overlap * (c1.mass / total_mass);
187
188 // 计算相对速度
189 let dvx = c2.vx - c1.vx;
190 let dvy = c2.vy - c1.vy;
191 let dv_dot_n = dvx * nx + dvy * ny;
192
193 if dv_dot_n > 0.0 {
194 return;
195 }
196
197 // 冲量
198 let impulse = -(1.0 + self.restitution) * dv_dot_n /
199 (1.0 / c1.mass + 1.0 / c2.mass);
200
201 c1.vx -= impulse * nx / c1.mass;
202 c1.vy -= impulse * ny / c1.mass;
203 c2.vx += impulse * nx / c2.mass;
204 c2.vy += impulse * ny / c2.mass;
205 }
206 }
207
208 // 获取位置数据 [x1, y1, r1, x2, y2, r2, ...]
209 pub fn get_positions(&self) -> Vec<f64> {
210 let mut result = Vec::with_capacity(self.circles.len() * 3);
211 for circle in &self.circles {
212 result.push(circle.x);
213 result.push(circle.y);
214 result.push(circle.radius);
215 }
216 result
217 }
218
219 // 应用力
220 pub fn apply_force(&mut self, x: f64, y: f64, fx: f64, fy: f64, radius: f64) {
221 for circle in &mut self.circles {
222 let dx = circle.x - x;
223 let dy = circle.y - y;
224 let dist = (dx * dx + dy * dy).sqrt();
225
226 if dist < radius {
227 let strength = 1.0 - (dist / radius);
228 circle.vx += fx * strength / circle.mass;
229 circle.vy += fy * strength / circle.mass;
230 }
231 }
232 }
233
234 pub fn clear(&mut self) {
235 self.circles.clear();
236 }
237}1<!DOCTYPE html>
2<html>
3 <body>
4 <canvas id="canvas" width="800" height="600"></canvas>
5 <div id="stats"></div>
6 <div>
7 <button onclick="addBalls(100)">添加100个球</button>
8 <button onclick="world.clear()">清空</button>
9 </div>
10 <script type="module" src="app.js"></script>
11 </body>
12</html>1import init, { PhysicsWorld } from "./pkg/physics_engine.js";
2
3let world;
4let canvas, ctx;
5let lastTime = 0;
6let fps = 0;
7let frameCount = 0;
8
9window.addEventListener("load", async () => {
10 await init();
11
12 canvas = document.getElementById("canvas");
13 ctx = canvas.getContext("2d");
14
15 world = new PhysicsWorld(canvas.width, canvas.height);
16
17 addBalls(50);
18
19 // 鼠标交互
20 let mouseX = 0,
21 mouseY = 0,
22 mouseDown = false;
23
24 canvas.addEventListener("mousemove", (e) => {
25 const rect = canvas.getBoundingClientRect();
26 mouseX = e.clientX - rect.left;
27 mouseY = e.clientY - rect.top;
28 });
29
30 canvas.addEventListener("mousedown", () => (mouseDown = true));
31 canvas.addEventListener("mouseup", () => (mouseDown = false));
32
33 // 游戏循环
34 let lastFpsUpdate = 0;
35
36 function gameLoop(currentTime) {
37 const dt = Math.min((currentTime - lastTime) / 1000, 0.016);
38 lastTime = currentTime;
39
40 // 鼠标吸引力
41 if (mouseDown) {
42 const fx = (mouseX - canvas.width / 2) * 100;
43 const fy = (mouseY - canvas.height / 2) * 100;
44 world.apply_force(mouseX, mouseY, fx, fy, 150);
45 }
46
47 // 更新物理
48 world.update(dt);
49
50 // 渲染
51 render();
52
53 // 统计
54 frameCount++;
55 if (currentTime - lastFpsUpdate > 1000) {
56 fps = frameCount;
57 frameCount = 0;
58 lastFpsUpdate = currentTime;
59
60 document.getElementById("stats").textContent = `
61 FPS: ${fps} | 物体: ${world.count()}
62 `;
63 }
64
65 requestAnimationFrame(gameLoop);
66 }
67
68 requestAnimationFrame(gameLoop);
69});
70
71function render() {
72 ctx.fillStyle = "#f0f0f0";
73 ctx.fillRect(0, 0, canvas.width, canvas.height);
74
75 const positions = world.get_positions();
76
77 for (let i = 0; i < positions.length; i += 3) {
78 const x = positions[i];
79 const y = positions[i + 1];
80 const r = positions[i + 2];
81
82 ctx.beginPath();
83 ctx.arc(x, y, r, 0, Math.PI * 2);
84 ctx.fillStyle = `hsl(${((i / 3) * 137.5) % 360}, 70%, 60%)`;
85 ctx.fill();
86 ctx.strokeStyle = "#333";
87 ctx.stroke();
88 }
89}
90
91window.addBalls = function (count) {
92 for (let i = 0; i < count; i++) {
93 const x = Math.random() * canvas.width;
94 const y = (Math.random() * canvas.height) / 2;
95 const radius = 5 + Math.random() * 15;
96 const mass = radius * radius * Math.PI;
97
98 world.add_circle(x, y, radius, mass);
99 }
100};1const PHYSICS_DT = 1 / 60;
2let accumulator = 0;
3
4function gameLoop(currentTime) {
5 const frameDt = (currentTime - lastTime) / 1000;
6 lastTime = currentTime;
7
8 accumulator += frameDt;
9
10 // 可能执行多次物理更新
11 while (accumulator >= PHYSICS_DT) {
12 world.update(PHYSICS_DT);
13 accumulator -= PHYSICS_DT;
14 }
15
16 render();
17 requestAnimationFrame(gameLoop);
18}大量对象时用四叉树:
1pub struct QuadTree {
2 bounds: (f64, f64, f64, f64),
3 capacity: usize,
4 objects: Vec<usize>,
5 divided: bool,
6 children: Option<Box<[QuadTree; 4]>>,
7}
8
9impl QuadTree {
10 pub fn query(&self, x: f64, y: f64, w: f64, h: f64) -> Vec<usize> {
11 // 只返回可能碰撞的对象
12 // 从 O(n²) 降到 O(n log n)
13 }
14}1pub struct ObjectPool {
2 active: Vec<Circle>,
3 inactive: Vec<Circle>,
4}
5
6impl ObjectPool {
7 pub fn spawn(&mut self, x: f64, y: f64, r: f64) {
8 if let Some(mut obj) = self.inactive.pop() {
9 obj.x = x;
10 obj.y = y;
11 obj.radius = r;
12 self.active.push(obj);
13 } else {
14 self.active.push(Circle::new(x, y, r, 1.0));
15 }
16 }
17
18 pub fn despawn(&mut self, index: usize) {
19 let obj = self.active.swap_remove(index);
20 self.inactive.push(obj);
21 }
22}1function createExplosion(x, y) {
2 for (let i = 0; i < 100; i++) {
3 const angle = Math.random() * Math.PI * 2;
4 const speed = 200 + Math.random() * 300;
5 const vx = Math.cos(angle) * speed;
6 const vy = Math.sin(angle) * speed;
7
8 world.add_circle(x, y, 2, 0.1);
9 // 设置速度...
10 }
11}1// 弹球游戏
2class BallGame {
3 constructor() {
4 this.world = new PhysicsWorld(800, 600);
5
6 // 添加边界
7 // 添加障碍物
8 // 添加玩家控制的板
9 }
10
11 update(dt) {
12 this.world.update(dt);
13
14 // 检查游戏逻辑
15 this.checkScore();
16 this.checkGameOver();
17 }
18}适合:
不适合: