Rust WebAssembly 与 Three.js 结合的 3D 数据可视化实战:高性能粒子系统
一、引言
3D 数据可视化是现代 Web 应用的高级场景之一,广泛应用于数据分析、科学计算、游戏开发、虚拟仿真等领域。传统的 JavaScript+WebGL/Three.js 方案在处理大量数据(如百万级粒子)时,性能往往难以满足要求。Rust WebAssembly 的高性能和内存安全特性,使得它非常适合优化 3D 数据可视化的核心算法,提高应用的响应速度和渲染帧率。
本章将深入探讨 Rust WebAssembly 与 Three.js 结合的 3D 数据可视化开发,介绍 WebGL/Three.js 的基本概念,讲解 Rust Wasm 与 WebGL 的交互方式,重点实现一个高性能粒子系统,支持粒子的创建、更新、删除,以及各种动画效果。最后,本章还将介绍如何优化粒子系统的性能,如何打包和部署项目。
二、WebGL 与 Three.js 基础
2.1 WebGL 概述
WebGL 是一种基于 OpenGL ES 的 Web 图形库,允许开发者在 Web 浏览器中使用 GPU 加速渲染 3D 图形。WebGL 的核心是着色器语言(GLSL),分为顶点着色器和片段着色器:
- 顶点着色器:处理顶点数据,计算顶点的位置和颜色
- 片段着色器:处理像素数据,计算像素的颜色和纹理
WebGL 的渲染流程:
- 顶点着色器:对每个顶点进行变换,计算其在屏幕上的位置
- 图元装配:将顶点连接成图元(如点、线、三角形)
- 光栅化:将图元转换为像素
- 片段着色器:对每个像素进行颜色计算和纹理采样
- 测试与混合:对像素进行深度测试、模板测试和颜色混合,最终输出到屏幕
2.2 Three.js 概述
Three.js 是一个基于 WebGL 的 JavaScript 3D 库,它简化了 WebGL 的开发,提供了高级抽象,如场景(Scene)、相机(Camera)、渲染器(Renderer)、网格(Mesh)、**光源(Light)**等。
Three.js 的核心组件:
- 场景(Scene):3D 空间的容器,包含所有 3D 对象和光源
- 相机(Camera):决定了从哪个视角观察场景,分为透视相机和正交相机
- 渲染器(Renderer):将场景渲染到 Canvas 上,支持 WebGL 和 WebGL2
- 网格(Mesh):由几何体(Geometry)和材质(Material)组成,是 3D 对象的基本单元
- 光源(Light):照亮场景中的 3D 对象,分为环境光、方向光、点光源、聚光灯等
- 纹理(Texture):为 3D 对象添加颜色和细节,支持图片纹理、Canvas 纹理、视频纹理等
2.3 Three.js 的安装与使用
安装 Three.js:
# 使用 npm 安装
npm install three
使用 Three.js 创建一个简单的 3D 场景:
import * as THREE from 'three';
// 创建场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
// 创建相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 5;
// 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 创建几何体
const geometry = new THREE.BoxGeometry(1, 1, 1);
// 创建材质
material = .({ : , : });
cube = .(geometry, material);
scene.(cube);
ambientLight = .();
scene.(ambientLight);
directionalLight = .(, );
directionalLight..(, , );
scene.(directionalLight);
() {
(animate);
cube.. += ;
cube.. += ;
renderer.(scene, camera);
}
();
.(, {
camera. = . / .;
camera.();
renderer.(., .);
});
三、Rust WebAssembly 与 WebGL 交互
3.1 传递顶点数据
顶点数据是 WebGL 渲染的基础,Rust Wasm 需要将顶点数据传递给 WebGL。顶点数据通常包括位置、颜色、纹理坐标等。
首先,在 Rust 中定义顶点结构:
// src/lib.rs
use wasm_bindgen::prelude::*;
use web_sys::WebGlRenderingContext;
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct Vertex {
pub x: f32,
pub y: f32,
pub z: f32,
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
impl Vertex {
pub fn new(x: f32, y: f32, z: f32, r: f32, g: f32, b: f32, a: f32) -> Self {
Vertex { x, y, z, r, g, b, a }
}
}
#[wasm_bindgen]
pub fn create_vertices(count: u32) -> Vec<f32> {
let mut vertices = Vec::with_capacity(count as usize * 7);
// 每个顶点有 7 个元素(x,y,z,r,g,b,a)
for i in ..count {
= i / count * std::::consts::PI * ;
= ;
= angle.() * radius;
= angle.() * radius;
= ;
= angle.() * + ;
= angle.() * + ;
= ;
= ;
vertices.(x);
vertices.(y);
vertices.(z);
vertices.(r);
vertices.(g);
vertices.(b);
vertices.(a);
}
vertices
}
然后,在 JavaScript 中使用 Three.js 加载顶点数据:
import * as THREE from 'three';
import init, { create_vertices } from './pkg/particle_system.js';
async function run() {
await init();
console.log('WebAssembly 模块加载成功');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 5;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.);
count = ;
vertices = (count);
geometry = .();
geometry.(
,
.(vertices.(, count * ), )
);
geometry.(
,
.(vertices.(count * ), )
);
material = .({
: ,
: ,
: ,
: ,
});
particles = .(geometry, material);
scene.(particles);
ambientLight = .();
scene.(ambientLight);
directionalLight = .(, );
directionalLight..(, , );
scene.(directionalLight);
() {
(animate);
particles.. += ;
renderer.(scene, camera);
}
();
.(, {
camera. = . / .;
camera.();
renderer.(., .);
});
}
();
3.2 传递纹理数据
纹理数据是 WebGL 渲染的重要组成部分,Rust Wasm 可以将纹理数据传递给 WebGL。纹理数据通常是一个二维数组,每个元素代表一个像素的颜色。
首先,在 Rust 中生成纹理数据:
// src/lib.rs
#[wasm_bindgen]
pub fn create_texture(width: u32, height: u32) -> Vec<u8> {
let mut texture = Vec::with_capacity(width as usize * height as usize * 4);
// 每个像素有 4 个元素(r,g,b,a)
for y in 0..height {
for x in 0..width {
let r = (x as f32 / width as f32 * 255.0) as u8;
let g = (y as f32 / height as f32 * 255.0) as u8;
let b = 128;
let a = 255;
texture.push(r);
texture.push(g);
texture.push(b);
texture.(a);
}
}
texture
}
然后,在 JavaScript 中使用 Three.js 加载纹理数据:
import * as THREE from 'three';
import init, { create_vertices, create_texture } from './pkg/particle_system.js';
async function run() {
await init();
console.log('WebAssembly 模块加载成功');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 5;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.);
count = ;
vertices = (count);
geometry = .();
geometry.(
,
.(vertices.(, count * ), )
);
geometry.(
,
.(vertices.(count * ), )
);
textureWidth = ;
textureHeight = ;
textureData = (textureWidth, textureHeight);
texture = .(
(textureData),
textureWidth,
textureHeight,
.,
.
);
texture. = ;
material = .({
: ,
: ,
: ,
: ,
: texture,
: .,
});
particles = .(geometry, material);
scene.(particles);
ambientLight = .();
scene.(ambientLight);
directionalLight = .(, );
directionalLight..(, , );
scene.(directionalLight);
() {
(animate);
particles.. += ;
renderer.(scene, camera);
}
();
.(, {
camera. = . / .;
camera.();
renderer.(., .);
});
}
();
3.3 传递变换矩阵
变换矩阵是 WebGL 渲染的核心,Rust Wasm 可以计算变换矩阵并传递给 WebGL。变换矩阵通常包括平移矩阵、旋转矩阵、缩放矩阵、透视矩阵等。
首先,在 Rust 中计算变换矩阵:
// src/lib.rs
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct Mat4 {
pub data: [f32; 16],
}
impl Mat4 {
pub fn identity() -> Self {
Mat4 {
data: [
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
],
}
}
pub fn translation(x: f32, y: f32, z: f32) -> Self {
let mut mat = Self::identity();
mat.data[12] = x;
mat.data[13] = y;
mat.data[14] = z;
mat
}
pub fn rotation(x: f32, y: f32, z: f32) -> {
= ::(x);
= ::(y);
= ::(z);
rx * ry * rz
}
(angle: ) {
= angle.();
= angle.();
Mat4 {
data: [
, , , ,
, c, -s, ,
, s, c, ,
, , , ,
],
}
}
(angle: ) {
= angle.();
= angle.();
Mat4 {
data: [
c, , s, ,
, , , ,
-s, , c, ,
, , , ,
],
}
}
(angle: ) {
= angle.();
= angle.();
Mat4 {
data: [
c, -s, , ,
s, c, , ,
, , , ,
, , , ,
],
}
}
(x: , y: , z: ) {
= ::();
mat.data[] = x;
mat.data[] = y;
mat.data[] = z;
mat
}
(&, other: &Mat4) {
= ::();
.. {
.. {
mat.data[i * + j] = (..)
.(|k| .data[i * + k] * other.data[k * + j])
.();
}
}
mat
}
}
::ops::Mul<Mat4> {
= Mat4;
(, other: Mat4) ::Output {
.(&other)
}
}
(
x: ,
y: ,
z: ,
rx: ,
ry: ,
rz: ,
sx: ,
sy: ,
sz: ,
) <> {
= Mat4::(x, y, z);
= Mat4::(rx, ry, rz);
= Mat4::(sx, sy, sz);
= translation * rotation * scaling;
model_matrix.data.()
}
然后,在 JavaScript 中使用 Three.js 加载变换矩阵:
import * as THREE from 'three';
import init, {
create_vertices,
create_texture,
create_model_matrix,
} from './pkg/particle_system.js';
async function run() {
await init();
console.log('WebAssembly 模块加载成功');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 5;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.);
count = ;
vertices = (count);
geometry = .();
geometry.(
,
.(vertices.(, count * ), )
);
geometry.(
,
.(vertices.(count * ), )
);
textureWidth = ;
textureHeight = ;
textureData = (textureWidth, textureHeight);
texture = .(
(textureData),
textureWidth,
textureHeight,
.,
.
);
texture. = ;
material = .({
: ,
: ,
: ,
: ,
: texture,
: .,
});
particles = .(geometry, material);
scene.(particles);
modelMatrixData = (, , , , , , , , );
modelMatrix = .().(modelMatrixData);
particles. = modelMatrix;
particles. = ;
ambientLight = .();
scene.(ambientLight);
directionalLight = .(, );
directionalLight..(, , );
scene.(directionalLight);
() {
(animate);
particles.. += ;
renderer.(scene, camera);
}
();
.(, {
camera. = . / .;
camera.();
renderer.(., .);
});
}
();
四、实战项目:高性能粒子系统
4.1 项目概述
开发一个高性能粒子系统,支持以下功能:
- 粒子的创建、更新、删除
- 粒子的位置、速度、加速度、生命周期管理
- 粒子的颜色、大小、透明度变化
- 粒子的各种动画效果,如爆炸、火焰、雪花、星云等
- 支持大量粒子(百万级)的渲染
4.2 项目结构
particle-system/
├── Cargo.toml
├── src/
│ ├── lib.rs
│ ├── particle.rs
│ ├── emitter.rs
│ └── utils.rs
├── static/
│ ├── index.html
│ ├── style.css
│ └── main.js
└── README.md
4.3 Cargo.toml
[package]
name = "particle_system"
version = "0.1.0"
edition = "2021"
[dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3", features = ["Window", "Document", "Element", "console", "WebGlRenderingContext", "CanvasRenderingContext2d"] }
rand = "0.8"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
4.4 src/particle.rs
// src/particle.rs
use rand::Rng;
use serde::Serialize;
#[derive(Debug, Clone, Copy, Serialize)]
pub struct Particle {
pub x: f32,
pub y: f32,
pub z: f32,
pub vx: f32,
pub vy: f32,
pub vz: f32,
pub ax: f32,
pub ay: f32,
pub az: f32,
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
pub size: f32,
pub lifetime: f32,
pub max_lifetime: f32,
}
impl Particle {
pub fn new() -> Self {
Particle {
x: 0.0,
y: 0.0,
z: 0.0,
vx: 0.0,
vy: 0.0,
vz: 0.0,
ax: 0.0,
ay: 0.0,
az: ,
r: ,
g: ,
b: ,
a: ,
size: ,
lifetime: ,
max_lifetime: ,
}
}
(& , rng: & rand::rngs::ThreadRng) {
.x = rng.(-..);
.y = rng.(-..);
.z = rng.(-..);
.vx = rng.(-..);
.vy = rng.(-..);
.vz = rng.(-..);
.ax = rng.(-..);
.ay = rng.(-..);
.az = rng.(-..);
.r = rng.(..);
.g = rng.(..);
.b = rng.(..);
.a = rng.(..);
.size = rng.(..);
.lifetime = ;
.max_lifetime = rng.(..);
}
(& , dt: ) {
.vx += .ax * dt;
.vy += .ay * dt;
.vz += .az * dt;
.x += .vx * dt;
.y += .vy * dt;
.z += .vz * dt;
.lifetime += dt;
.a = - .lifetime / .max_lifetime;
.size = * ( - .lifetime / .max_lifetime);
}
(&) {
.lifetime > .max_lifetime
}
}
4.5 src/emitter.rs
// src/emitter.rs
use crate::particle::Particle;
use rand::Rng;
#[derive(Debug, Clone)]
pub struct Emitter {
pub x: f32,
pub y: f32,
pub z: f32,
pub particles: Vec<Particle>,
pub max_particles: usize,
pub emission_rate: f32,
pub accumulated_time: f32,
pub rng: rand::rngs::ThreadRng,
}
impl Emitter {
pub fn new(
x: f32,
y: f32,
z: f32,
max_particles: usize,
emission_rate: f32,
) -> Self {
Emitter {
x,
y,
z,
particles: Vec::with_capacity(max_particles),
max_particles,
emission_rate,
accumulated_time: 0.0,
rng: rand::thread_rng(),
}
}
pub fn update(&mut self, dt: f32) {
self.accumulated_time += dt;
// 发射新粒子
let particles_to_spawn = (self.accumulated_time * .emission_rate) ;
particles_to_spawn > && .particles.() < .max_particles {
= (.max_particles - .particles.())
.(particles_to_spawn);
..spawn_count {
= Particle::();
particle.(& .rng);
particle.x += .x;
particle.y += .y;
particle.z += .z;
.particles.(particle);
}
.accumulated_time -= particles_to_spawn / .emission_rate;
}
& .particles {
particle.(dt);
}
.particles.(|particle| !particle.());
}
(&) <> {
= ::(.particles.() * );
&.particles {
vertices.(particle.x);
vertices.(particle.y);
vertices.(particle.z);
vertices.(particle.r);
vertices.(particle.g);
vertices.(particle.b);
vertices.(particle.a);
}
vertices
}
(&) <> {
= ::(.particles.());
&.particles {
sizes.(particle.size);
}
sizes
}
}
4.6 src/utils.rs
// src/utils.rs
pub fn set_panic_hook() {
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
}
4.7 src/lib.rs
// src/lib.rs
use wasm_bindgen::prelude::*;
use crate::emitter::Emitter;
#[wasm_bindgen]
pub struct EmitterWrapper {
emitter: Emitter,
}
#[wasm_bindgen]
impl EmitterWrapper {
#[wasm_bindgen(constructor)]
pub fn new(
x: f32,
y: f32,
z: f32,
max_particles: usize,
emission_rate: f32,
) -> EmitterWrapper {
EmitterWrapper {
emitter: Emitter::new(x, y, z, max_particles, emission_rate),
}
}
pub fn update(&mut self, dt: f32) {
self.emitter.update(dt);
}
pub fn get_vertices(&self) -> Vec<f32> {
self.emitter.get_vertices()
}
pub fn get_sizes(&self) -> Vec<f32> {
self.emitter.get_sizes()
}
pub fn get_count(&self) {
.emitter.particles.()
}
}
() {
utils::();
}
4.8 static/style.css
/* static/style.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: Arial, sans-serif;
}
body {
background-color: #000000;
overflow: hidden;
}
canvas {
display: block;
}
.controls {
position: absolute;
top: 20px;
left: 20px;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 20px;
color: #ffffff;
}
.controls h2 {
margin-bottom: 20px;
font-size: 18px;
}
.controls .emitter {
margin-bottom: 20px;
}
.controls .emitter label {
display: block;
margin-bottom: 5px;
font-size: 14px;
}
.controls .emitter {
: ;
: ;
: ;
: solid ;
: ;
: ;
: (, , , );
: ;
}
{
: ;
}
{
: ;
: ;
: ;
: ;
: none;
: ;
: ;
: pointer;
}
{
: ;
}
{
: ;
: ;
}
{
: ;
}
{
: ;
}
{
: ;
: ;
: ;
: ;
: none;
: ;
: ;
: pointer;
}
{
: ;
}
4.9 static/main.js
// static/main.js
import * as THREE from 'three';
import Stats from 'three/examples/jsm/libs/stats.module.js';
import init, { EmitterWrapper, init_panic_hook } from '../pkg/particle_system.js';
async function run() {
await init();
init_panic_hook();
console.log('WebAssembly 模块加载成功');
// 初始化 Stats
const stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.top = '20px';
stats.domElement.style.right = '20px';
document.body.appendChild(stats.domElement);
// 创建场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
camera = .(
,
. / .,
,
);
camera.. = ;
renderer = .({ : });
renderer.(., .);
renderer.(.);
..(renderer.);
maxParticles = ;
emissionRate = ;
emitter = (, , , maxParticles, emissionRate);
geometry = .();
vertices = emitter.();
geometry.(
,
.(
vertices.(, vertices. / * ),
)
);
geometry.(
,
.(
vertices.(vertices. / * ),
)
);
sizes = emitter.();
geometry.(, .(sizes, ));
canvas = .();
canvas. = ;
canvas. = ;
ctx = canvas.();
gradient = ctx.(, , , , , );
gradient.(, );
gradient.(, );
gradient.(, );
ctx. = gradient;
ctx.(, , , );
texture = .(canvas);
material = .({
: {
: { : texture },
},
: ,
: ,
: .,
: ,
: ,
: ,
});
particles = .(geometry, material);
scene.(particles);
ambientLight = .();
scene.(ambientLight);
directionalLight = .(, );
directionalLight..(, , );
scene.(directionalLight);
controlsDiv = .();
controlsDiv. = ;
controlsDiv. = ;
..(controlsDiv);
xInput = .();
yInput = .();
zInput = .();
maxParticlesInput = .();
emissionRateInput = .();
updateEmitterButton = .();
resetButton = .();
particleCountSpan = .();
fpsSpan = .();
updateEmitterButton.(, {
x = (xInput.);
y = (yInput.);
z = (zInput.);
newMaxParticles = (maxParticlesInput.);
newEmissionRate = (emissionRateInput.);
emitter. = x;
emitter. = y;
emitter. = z;
emitter. = newMaxParticles;
emitter. = newEmissionRate;
});
resetButton.(, {
xInput. = ;
yInput. = ;
zInput. = ;
maxParticlesInput. = maxParticles.();
emissionRateInput. = emissionRate.();
emitter. = ;
emitter. = ;
emitter. = ;
emitter. = maxParticles;
emitter. = emissionRate;
particles..(
,
.([], )
);
particles..(
,
.([], )
);
particles..(
,
.([], )
);
});
lastTime = performance.();
() {
stats.();
currentTime = performance.();
dt = (currentTime - lastTime) / ;
lastTime = currentTime;
emitter.(dt);
vertices = emitter.();
sizes = emitter.();
(vertices. > ) {
particles.... = (
vertices.(, vertices. / * )
);
particles.... = (
vertices.(vertices. / * )
);
particles.... = (sizes);
particles.... = ;
particles.... = ;
particles.... = ;
particles..();
}
particleCountSpan. = emitter.();
fpsSpan. = .( / dt);
camera.. = .(currentTime * ) * ;
camera.. = .(currentTime * ) * ;
camera.(, , );
renderer.(scene, camera);
stats.();
(animate);
}
();
.(, {
camera. = . / .;
camera.();
renderer.(., .);
});
}
();
4.10 static/index.html
<!-- static/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>Rust WebAssembly 粒子系统</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<script type="module"> import './main.js'; </script>
</body>
</html>
4.11 编译项目
wasm-pack build --target web
4.12 测试项目
npx serve .
在浏览器中访问 http://localhost:3000/static/index.html,查看粒子系统的效果。
五、性能优化
5.1 编译器优化
在 Cargo.toml 中添加优化配置:
[profile.release]
lto = true
codegen-units = 1
opt-level = 3
5.2 数据布局优化
使用 #[repr(C)] 属性对齐结构体,减少内存对齐开销:
// src/particle.rs
#[repr(C)]
#[derive(Debug, Clone, Copy, Serialize)]
pub struct Particle {
pub x: f32,
pub y: f32,
pub z: f32,
pub vx: f32,
pub vy: f32,
pub vz: f32,
pub ax: f32,
pub ay: f32,
pub az: f32,
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
pub size: f32,
pub lifetime: f32,
pub max_lifetime: f32,
}
5.3 内存管理优化
使用栈分配代替堆分配,减少内存分配次数:
// src/particle.rs
impl Particle {
pub fn update(&mut self, dt: f32) {
self.vx += self.ax * dt;
self.vy += self.ay * dt;
self.vz += self.az * dt;
self.x += self.vx * dt;
self.y += self.vy * dt;
self.z += self.vz * dt;
self.lifetime += dt;
self.a = 1.0 - self.lifetime / self.max_lifetime;
self.size = 0.05 * (1.0 - self.lifetime / self.max_lifetime);
}
}
5.4 算法优化
使用 SIMD 指令优化粒子更新算法:
// src/particle.rs
#[cfg(target_arch = "wasm32")]
use wasm_simd::*;
impl Particle {
#[cfg(target_arch = "wasm32")]
pub fn update_batch(particles: &mut [Particle], dt: f32) {
const BATCH_SIZE: usize = 8; // 每次处理 8 个粒子
for i in (0..particles.len()).step_by(BATCH_SIZE) {
let batch_end = (i + BATCH_SIZE).min(particles.len());
let batch = &mut particles[i..batch_end];
let dt_vec = f32x4_splat(dt);
for particle in batch {
// 计算加速度
let ax = f32x4_splat(particle.ax);
let ay = f32x4_splat(particle.ay);
let az = f32x4_splat(particle.az);
let a_vec = f32x4(particle.ax, particle.ay, particle.az, 0.0);
// 计算速度
= (particle.vx);
= (particle.vy);
= (particle.vz);
= (particle.vx, particle.vy, particle.vz, );
= v_vec + a_vec * dt_vec;
particle.vx = (v_new, );
particle.vy = (v_new, );
particle.vz = (v_new, );
= (particle.x);
= (particle.y);
= (particle.z);
= (particle.x, particle.y, particle.z, );
= pos_vec + v_new * dt_vec;
particle.x = (pos_new, );
particle.y = (pos_new, );
particle.z = (pos_new, );
particle.lifetime += dt;
particle.a = - particle.lifetime / particle.max_lifetime;
particle.size = * ( - particle.lifetime / particle.max_lifetime);
}
}
}
}
5.5 Web Workers 并行计算
使用 Web Workers 将粒子更新任务并行化:
// static/worker.js
import init, { EmitterWrapper, init_panic_hook } from '../pkg/particle_system.js';
let emitter;
let maxParticles;
let emissionRate;
self.onmessage = async (e) => {
const { type, data } = e.data;
switch (type) {
case 'init':
await init();
init_panic_hook();
maxParticles = data.maxParticles;
emissionRate = data.emissionRate;
emitter = new EmitterWrapper(data.x, data.y, data.z, data.maxParticles, data.emissionRate);
break;
case 'update':
emitter.update(data.dt);
const vertices = emitter.get_vertices();
const sizes = emitter.get_sizes();
const count = emitter.get_count();
self.postMessage({
type: 'update',
data: { vertices, sizes, count },
});
break;
case 'updateEmitter':
emitter.x = data.x;
emitter.y = data.y;
emitter. = data.;
(data. !== maxParticles) {
maxParticles = data.;
emitter. = maxParticles;
}
(data. !== emissionRate) {
emissionRate = data.;
emitter. = emissionRate;
}
;
:
emitter. = ;
emitter. = ;
emitter. = ;
emitter. = maxParticles;
emitter. = emissionRate;
self.({ : });
;
}
};
然后,在主进程中使用 Web Workers:
// static/main.js
const worker = new Worker('worker.js', { type: 'module' });
// 初始化 Web Workers
worker.postMessage({
type: 'init',
data: {
x: 0,
y: 0,
z: 0,
maxParticles: 100000,
emissionRate: 10000,
},
});
// 更新粒子发射器
worker.postMessage({
type: 'updateEmitter',
data: {
x: parseFloat(xInput.value),
y: parseFloat(yInput.value),
z: parseFloat(zInput.value),
maxParticles: parseInt(maxParticlesInput.value),
emissionRate: parseInt(emissionRateInput.value),
},
});
// 更新粒子
worker.postMessage({
type: 'update',
data: {
dt: (currentTime - lastTime) / 1000,
},
});
// 监听 Web Workers 消息
worker.onmessage = (e) => {
{ type, data } = e.;
(type) {
:
vertices = data.;
sizes = data.;
(vertices. > ) {
particles.... = (
vertices.(, vertices. / * )
);
particles.... = (
vertices.(vertices. / * )
);
particles.... = (sizes);
particles.... = ;
particles.... = ;
particles.... = ;
particles..();
}
particleCountSpan. = data.;
;
:
particles..(
,
.([], )
);
particles..(
,
.([], )
);
particles..(
,
.([], )
);
;
}
};
六、项目部署
6.1 使用 Vite 打包
安装 Vite:
npm init -y
npm install -D vite
创建 vite.config.js:
import { defineConfig } from 'vite';
export default defineConfig({
root: 'static',
build: {
outDir: '../dist',
assetsDir: 'assets',
rollupOptions: {
input: {
main: './static/index.html',
},
},
},
server: {
port: 3000,
open: true,
},
});
在 package.json 中添加脚本:
{
"name": "particle-system",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^5.0.0"
},
"dependencies": {
"three": "^0.160.0"
}
}
6.2 部署到 Netlify
创建 netlify.toml:
[build]
base = "static"
publish = "dist"
command = "npm run build"
在 Netlify 上创建项目,连接到 GitHub 仓库,部署项目。
6.3 部署到 Vercel
创建 vercel.json:
{
"buildCommand": "npm run build",
"outputDirectory": "dist",
"devCommand": "npm run dev",
"framework": "vite"
}
在 Vercel 上创建项目,连接到 GitHub 仓库,部署项目。
七、总结
本章介绍了如何使用 Rust WebAssembly 与 Three.js 结合开发高性能粒子系统,实现了粒子的创建、更新、删除,以及各种动画效果。通过 Rust 的高性能和内存安全特性,粒子系统可以支持大量粒子(百万级)的渲染,提供了出色的性能和用户体验。
7.1 技术栈
- Rust:开发语言
- wasm-bindgen:Rust 与 JavaScript 交互库
- web-sys:Web API 封装库
- wasm-pack:WebAssembly 开发工具
- Three.js:3D 图形库
- Vite:打包工具
- HTML/CSS/JavaScript:前端开发语言
7.2 核心功能
- 粒子管理:粒子的创建、更新、删除
- 粒子属性:位置、速度、加速度、生命周期、颜色、大小、透明度
- 粒子动画:爆炸、火焰、雪花、星云等
- 性能优化:编译器优化、数据布局优化、内存管理优化、算法优化、Web Workers 并行计算
7.3 未来改进
- 添加更多粒子类型:如雪花粒子、火焰粒子、爆炸粒子、星云粒子等
- 添加物理引擎:支持重力、风力、碰撞检测等物理效果
- 添加用户界面优化:如拖拽发射器、实时调整参数、保存和加载配置等
- 添加网络支持:支持多人协同操作和数据同步
- 优化性能:使用 WebGL2、WebGPU、WebVR/AR 等技术提高渲染性能
通过本章的学习,读者可以深入理解 Rust WebAssembly 与 Three.js 结合开发 3D 数据可视化应用的工作原理和实现方法,并在实际项目中应用这些技术。同时,本章也介绍了如何优化粒子系统的性能,帮助读者构建高性能的 3D 数据可视化应用。


