跳到主要内容
Rust WebAssembly 与 Three.js 结合的 3D 数据可视化实战:高性能粒子系统 | 极客日志
Rust 大前端 算法
Rust WebAssembly 与 Three.js 结合的 3D 数据可视化实战:高性能粒子系统 介绍如何利用 Rust WebAssembly 与 Three.js 结合开发高性能 3D 粒子系统。涵盖 WebGL 基础、Rust Wasm 交互(顶点/纹理/矩阵数据传递)、粒子系统实现(发射器、生命周期管理)及性能优化(编译器、SIMD、Web Workers)。最后提供 Vite 打包及 Netlify/Vercel 部署方案,适合需要处理百万级粒子渲染的前端开发者。
心动瞬间 发布于 2026/4/6 更新于 2026/5/20 24 浏览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 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 );
const material = new THREE .MeshPhongMaterial ({ color : 0x00ff00 , shininess : 100 });
const cube = new THREE .Mesh (geometry, material);
scene.add (cube);
const ambientLight = new THREE .AmbientLight (0x404040 );
scene.add (ambientLight);
const directionalLight = new THREE .DirectionalLight (0xffffff , 0.8 );
directionalLight.position .set (1 , 1 , 1 );
scene.add (directionalLight);
function animate ( ) {
requestAnimationFrame (animate);
cube.rotation .x += 0.01 ;
cube.rotation .y += 0.01 ;
renderer.render (scene, camera);
}
animate ();
window .addEventListener ('resize' , () => {
camera.aspect = window .innerWidth / window .innerHeight ;
camera.updateProjectionMatrix ();
renderer.setSize (window .innerWidth , window .innerHeight );
});
三、Rust WebAssembly 与 WebGL 交互
3.1 传递顶点数据 顶点数据是 WebGL 渲染的基础,Rust Wasm 需要将顶点数据传递给 WebGL。顶点数据通常包括位置、颜色、纹理坐标等。
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 );
for i in 0 ..count {
let angle = i as f32 / count as f32 * std::f32 ::consts::PI * 2.0 ;
let radius = 2.0 ;
let x = angle.cos () * radius;
let y = angle.sin () * radius;
let z = 0.0 ;
let r = angle.cos () * 0.5 + 0.5 ;
let g = angle.sin () * 0.5 + 0.5 ;
let b = 0.5 ;
let a = 1.0 ;
vertices.push (x);
vertices.push (y);
vertices.push (z);
vertices.push (r);
vertices.push (g);
vertices.push (b);
vertices.push (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.domElement );
const count = 1000 ;
const vertices = create_vertices (count);
const geometry = new THREE .BufferGeometry ();
geometry.setAttribute (
'position' ,
new THREE .Float32BufferAttribute (vertices.slice (0 , count * 3 ), 3 )
);
geometry.setAttribute (
'color' ,
new THREE .Float32BufferAttribute (vertices.slice (count * 3 ), 4 )
);
const material = new THREE .PointsMaterial ({
size : 0.05 ,
vertexColors : true ,
transparent : true ,
opacity : 0.8 ,
});
const particles = new THREE .Points (geometry, material);
scene.add (particles);
const ambientLight = new THREE .AmbientLight (0x404040 );
scene.add (ambientLight);
const directionalLight = new THREE .DirectionalLight (0xffffff , 0.8 );
directionalLight.position .set (1 , 1 , 1 );
scene.add (directionalLight);
function animate ( ) {
requestAnimationFrame (animate);
particles.rotation .y += 0.005 ;
renderer.render (scene, camera);
}
animate ();
window .addEventListener ('resize' , () => {
camera.aspect = window .innerWidth / window .innerHeight ;
camera.updateProjectionMatrix ();
renderer.setSize (window .innerWidth , window .innerHeight );
});
}
run ();
3.2 传递纹理数据 纹理数据是 WebGL 渲染的重要组成部分,Rust Wasm 可以将纹理数据传递给 WebGL。纹理数据通常是一个二维数组,每个元素代表一个像素的颜色。
#[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 );
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.push (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.domElement );
const count = 1000 ;
const vertices = create_vertices (count);
const geometry = new THREE .BufferGeometry ();
geometry.setAttribute (
'position' ,
new THREE .Float32BufferAttribute (vertices.slice (0 , count * 3 ), 3 )
);
geometry.setAttribute (
'color' ,
new THREE .Float32BufferAttribute (vertices.slice (count * 3 ), 4 )
);
const textureWidth = 256 ;
const textureHeight = 256 ;
const textureData = create_texture (textureWidth, textureHeight);
const texture = new THREE .DataTexture (
new Uint8Array (textureData),
textureWidth,
textureHeight,
THREE .RGBAFormat ,
THREE .UnsignedByteType
);
texture.needsUpdate = true ;
const material = new THREE .PointsMaterial ({
size : 0.05 ,
vertexColors : true ,
transparent : true ,
opacity : 0.8 ,
map : texture,
blending : THREE .AdditiveBlending ,
});
const particles = new THREE .Points (geometry, material);
scene.add (particles);
const ambientLight = new THREE .AmbientLight (0x404040 );
scene.add (ambientLight);
const directionalLight = new THREE .DirectionalLight (0xffffff , 0.8 );
directionalLight.position .set (1 , 1 , 1 );
scene.add (directionalLight);
function animate ( ) {
requestAnimationFrame (animate);
particles.rotation .y += 0.005 ;
renderer.render (scene, camera);
}
animate ();
window .addEventListener ('resize' , () => {
camera.aspect = window .innerWidth / window .innerHeight ;
camera.updateProjectionMatrix ();
renderer.setSize (window .innerWidth , window .innerHeight );
});
}
run ();
3.3 传递变换矩阵 变换矩阵是 WebGL 渲染的核心,Rust Wasm 可以计算变换矩阵并传递给 WebGL。变换矩阵通常包括平移矩阵、旋转矩阵、缩放矩阵、透视矩阵等。
#[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 ) -> Self {
let rx = Self ::rotation_x (x);
let ry = Self ::rotation_y (y);
let rz = Self ::rotation_z (z);
rx * ry * rz
}
pub fn rotation_x (angle: f32 ) -> Self {
let c = angle.cos ();
let s = angle.sin ();
Mat4 {
data: [
1.0 , 0.0 , 0.0 , 0.0 ,
0.0 , c, -s, 0.0 ,
0.0 , s, c, 0.0 ,
0.0 , 0.0 , 0.0 , 1.0 ,
],
}
}
pub fn rotation_y (angle: f32 ) -> Self {
let c = angle.cos ();
let s = angle.sin ();
Mat4 {
data: [
c, 0.0 , s, 0.0 ,
0.0 , 1.0 , 0.0 , 0.0 ,
-s, 0.0 , c, 0.0 ,
0.0 , 0.0 , 0.0 , 1.0 ,
],
}
}
pub fn rotation_z (angle: f32 ) -> Self {
let c = angle.cos ();
let s = angle.sin ();
Mat4 {
data: [
c, -s, 0.0 , 0.0 ,
s, c, 0.0 , 0.0 ,
0.0 , 0.0 , 1.0 , 0.0 ,
0.0 , 0.0 , 0.0 , 1.0 ,
],
}
}
pub fn scaling (x: f32 , y: f32 , z: f32 ) -> Self {
let mut mat = Self ::identity ();
mat.data[0 ] = x;
mat.data[5 ] = y;
mat.data[10 ] = z;
mat
}
pub fn multiply (&self , other: &Mat4) -> Self {
let mut mat = Self ::identity ();
for i in 0 ..4 {
for j in 0 ..4 {
mat.data[i * 4 + j] = (0 ..4 )
.map (|k| self .data[i * 4 + k] * other.data[k * 4 + j])
.sum ();
}
}
mat
}
}
impl std ::ops::Mul<Mat4> for Mat4 {
type Output = Mat4;
fn mul (self , other: Mat4) -> Self ::Output {
self .multiply (&other)
}
}
#[wasm_bindgen]
pub fn create_model_matrix (
x: f32 ,
y: f32 ,
z: f32 ,
rx: f32 ,
ry: f32 ,
rz: f32 ,
sx: f32 ,
sy: f32 ,
sz: f32 ,
) -> Vec <f32 > {
let translation = Mat4::translation (x, y, z);
let rotation = Mat4::rotation (rx, ry, rz);
let scaling = Mat4::scaling (sx, sy, sz);
let model_matrix = translation * rotation * scaling;
model_matrix.data.to_vec ()
}
然后,在 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.domElement );
const count = 1000 ;
const vertices = create_vertices (count);
const geometry = new THREE .BufferGeometry ();
geometry.setAttribute (
'position' ,
new THREE .Float32BufferAttribute (vertices.slice (0 , count * 3 ), 3 )
);
geometry.setAttribute (
'color' ,
new THREE .Float32BufferAttribute (vertices.slice (count * 3 ), 4 )
);
const textureWidth = 256 ;
const textureHeight = 256 ;
const textureData = create_texture (textureWidth, textureHeight);
const texture = new THREE .DataTexture (
new Uint8Array (textureData),
textureWidth,
textureHeight,
THREE .RGBAFormat ,
THREE .UnsignedByteType
);
texture.needsUpdate = true ;
const material = new THREE .PointsMaterial ({
size : 0.05 ,
vertexColors : true ,
transparent : true ,
opacity : 0.8 ,
map : texture,
blending : THREE .AdditiveBlending ,
});
const particles = new THREE .Points (geometry, material);
scene.add (particles);
const modelMatrixData = create_model_matrix (0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 );
const modelMatrix = new THREE .Matrix4 ().fromArray (modelMatrixData);
particles.matrix = modelMatrix;
particles.matrixAutoUpdate = true ;
const ambientLight = new THREE .AmbientLight (0x404040 );
scene.add (ambientLight);
const directionalLight = new THREE .DirectionalLight (0xffffff , 0.8 );
directionalLight.position .set (1 , 1 , 1 );
scene.add (directionalLight);
function animate ( ) {
requestAnimationFrame (animate);
particles.rotation .y += 0.005 ;
renderer.render (scene, camera);
}
animate ();
window .addEventListener ('resize' , () => {
camera.aspect = window .innerWidth / window .innerHeight ;
camera.updateProjectionMatrix ();
renderer.setSize (window .innerWidth , window .innerHeight );
});
}
run ();
四、实战项目:高性能粒子系统
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
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: 0.0 ,
r: 1.0 ,
g: 1.0 ,
b: 1.0 ,
a: 1.0 ,
size: 0.05 ,
lifetime: 0.0 ,
max_lifetime: 10.0 ,
}
}
pub fn randomize (&mut self , rng: &mut rand::rngs::ThreadRng) {
self .x = rng.gen_range (-2.0 ..2.0 );
self .y = rng.gen_range (-2.0 ..2.0 );
self .z = rng.gen_range (-2.0 ..2.0 );
self .vx = rng.gen_range (-0.1 ..0.1 );
self .vy = rng.gen_range (-0.1 ..0.1 );
self .vz = rng.gen_range (-0.1 ..0.1 );
self .ax = rng.gen_range (-0.01 ..0.01 );
self .ay = rng.gen_range (-0.01 ..0.01 );
self .az = rng.gen_range (-0.01 ..0.01 );
self .r = rng.gen_range (0.0 ..1.0 );
self .g = rng.gen_range (0.0 ..1.0 );
self .b = rng.gen_range (0.0 ..1.0 );
self .a = rng.gen_range (0.5 ..1.0 );
self .size = rng.gen_range (0.01 ..0.1 );
self .lifetime = 0.0 ;
self .max_lifetime = rng.gen_range (5.0 ..15.0 );
}
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);
}
pub fn is_dead (&self ) -> bool {
self .lifetime > self .max_lifetime
}
}
4.5 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 * self .emission_rate) as usize ;
if particles_to_spawn > 0 && self .particles.len () < self .max_particles {
let spawn_count = (self .max_particles - self .particles.len ())
.min (particles_to_spawn);
for _ in 0 ..spawn_count {
let mut particle = Particle::new ();
particle.randomize (&mut self .rng);
particle.x += self .x;
particle.y += self .y;
particle.z += self .z;
self .particles.push (particle);
}
self .accumulated_time -= particles_to_spawn as f32 / self .emission_rate;
}
for particle in &mut self .particles {
particle.update (dt);
}
self .particles.retain (|particle| !particle.is_dead ());
}
pub fn get_vertices (&self ) -> Vec <f32 > {
let mut vertices = Vec ::with_capacity (self .particles.len () * 7 );
for particle in &self .particles {
vertices.push (particle.x);
vertices.push (particle.y);
vertices.push (particle.z);
vertices.push (particle.r);
vertices.push (particle.g);
vertices.push (particle.b);
vertices.push (particle.a);
}
vertices
}
pub fn get_sizes (&self ) -> Vec <f32 > {
let mut sizes = Vec ::with_capacity (self .particles.len ());
for particle in &self .particles {
sizes.push (particle.size);
}
sizes
}
}
4.6 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
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 ) -> usize {
self .emitter.particles.len ()
}
}
#[wasm_bindgen]
pub fn init_panic_hook () {
utils::set_panic_hook ();
}
4.8 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 input {
width : 100% ;
padding : 8px ;
margin-bottom : 10px ;
border : 1px solid #ddd ;
border-radius : 4px ;
font-size : 14px ;
background-color : rgba (255 , 255 , 255 , 0.2 );
color : #ffffff ;
}
.controls .emitter input ::placeholder {
color : #cccccc ;
}
.controls .emitter button {
width : 100% ;
padding : 8px 16px ;
background-color : #007bff ;
color : #ffffff ;
border : none;
border-radius : 4px ;
font-size : 14px ;
cursor : pointer;
}
.controls .emitter button :hover {
background-color : #0056b3 ;
}
.controls .stats {
margin-bottom : 20px ;
font-size : 14px ;
}
.controls .stats div {
margin-bottom : 5px ;
}
.controls .reset {
margin-bottom : 20px ;
}
.controls .reset button {
width : 100% ;
padding : 8px 16px ;
background-color : #6c757d ;
color : #ffffff ;
border : none;
border-radius : 4px ;
font-size : 14px ;
cursor : pointer;
}
.controls .reset button :hover {
background-color : #5a6268 ;
}
4.9 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 模块加载成功' );
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 );
const camera = new THREE .PerspectiveCamera (
75 ,
window .innerWidth / window .innerHeight ,
0.1 ,
1000
);
camera.position .z = 10 ;
const renderer = new THREE .WebGLRenderer ({ antialias : true });
renderer.setSize (window .innerWidth , window .innerHeight );
renderer.setPixelRatio (window .devicePixelRatio );
document .body .appendChild (renderer.domElement );
const maxParticles = 100000 ;
const emissionRate = 10000 ;
const emitter = new EmitterWrapper (0 , 0 , 0 , maxParticles, emissionRate);
const geometry = new THREE .BufferGeometry ();
const vertices = emitter.get_vertices ();
geometry.setAttribute (
'position' ,
new THREE .Float32BufferAttribute (
vertices.slice (0 , vertices.length / 7 * 3 ),
3
)
);
geometry.setAttribute (
'color' ,
new THREE .Float32BufferAttribute (
vertices.slice (vertices.length / 7 * 3 ),
4
)
);
const sizes = emitter.get_sizes ();
geometry.setAttribute ('size' , new THREE .Float32BufferAttribute (sizes, 1 ));
const canvas = document .createElement ('canvas' );
canvas.width = 32 ;
canvas.height = 32 ;
const ctx = canvas.getContext ('2d' );
const gradient = ctx.createRadialGradient (16 , 16 , 0 , 16 , 16 , 16 );
gradient.addColorStop (0 , 'rgba(255, 255, 255, 1)' );
gradient.addColorStop (0.5 , 'rgba(255, 255, 255, 0.5)' );
gradient.addColorStop (1 , 'rgba(255, 255, 255, 0)' );
ctx.fillStyle = gradient;
ctx.fillRect (0 , 0 , 32 , 32 );
const texture = new THREE .CanvasTexture (canvas);
const material = new THREE .ShaderMaterial ({
uniforms : {
pointTexture : { value : texture },
},
vertexShader : `
attribute float size;
attribute vec4 color;
varying vec4 vColor;
void main() {
vColor = color;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
gl_PointSize = size * (300.0 / -mvPosition.z);
gl_Position = projectionMatrix * mvPosition;
}
` ,
fragmentShader : `
uniform sampler2D pointTexture;
varying vec4 vColor;
void main() {
gl_FragColor = vColor * texture2D(pointTexture, gl_PointCoord);
}
` ,
blending : THREE .AdditiveBlending ,
depthTest : false ,
transparent : true ,
vertexColors : true ,
});
const particles = new THREE .Points (geometry, material);
scene.add (particles);
const ambientLight = new THREE .AmbientLight (0x404040 );
scene.add (ambientLight);
const directionalLight = new THREE .DirectionalLight (0xffffff , 0.8 );
directionalLight.position .set (1 , 1 , 1 );
scene.add (directionalLight);
const controlsDiv = document .createElement ('div' );
controlsDiv.className = 'controls' ;
controlsDiv.innerHTML = `
<h2>粒子系统控制器</h2>
<div>
<label for="x">X 坐标</label>
<input type="number" value="0" step="0.1">
<label for="y">Y 坐标</label>
<input type="number" value="0" step="0.1">
<label for="z">Z 坐标</label>
<input type="number" value="0" step="0.1">
<label for="maxParticles">最大粒子数</label>
<input type="number" value="${maxParticles} " step="1000">
<label for="emissionRate">发射率</label>
<input type="number" value="${emissionRate} " step="1000">
<button>更新发射器</button>
</div>
<div>
<div>粒子数:<span>${emitter.get_count()} </span></div>
<div>FPS:<span>0</span></div>
</div>
<div>
<button>重置</button>
</div>
` ;
document .body .appendChild (controlsDiv);
const xInput = document .getElementById ('x' );
const yInput = document .getElementById ('y' );
const zInput = document .getElementById ('z' );
const maxParticlesInput = document .getElementById ('maxParticles' );
const emissionRateInput = document .getElementById ('emissionRate' );
const updateEmitterButton = document .getElementById ('updateEmitter' );
const resetButton = document .getElementById ('reset' );
const particleCountSpan = document .getElementById ('particleCount' );
const fpsSpan = document .getElementById ('fps' );
updateEmitterButton.addEventListener ('click' , () => {
const x = parseFloat (xInput.value );
const y = parseFloat (yInput.value );
const z = parseFloat (zInput.value );
const newMaxParticles = parseInt (maxParticlesInput.value );
const newEmissionRate = parseInt (emissionRateInput.value );
emitter.x = x;
emitter.y = y;
emitter.z = z;
emitter.max_particles = newMaxParticles;
emitter.emission_rate = newEmissionRate;
});
resetButton.addEventListener ('click' , () => {
xInput.value = '0' ;
yInput.value = '0' ;
zInput.value = '0' ;
maxParticlesInput.value = maxParticles.toString ();
emissionRateInput.value = emissionRate.toString ();
emitter.x = 0 ;
emitter.y = 0 ;
emitter.z = 0 ;
emitter.max_particles = maxParticles;
emitter.emission_rate = emissionRate;
particles.geometry .setAttribute ('position' , new THREE .Float32BufferAttribute ([], 3 ));
particles.geometry .setAttribute ('color' , new THREE .Float32BufferAttribute ([], 4 ));
particles.geometry .setAttribute ('size' , new THREE .Float32BufferAttribute ([], 1 ));
});
let lastTime = performance.now ();
function animate ( ) {
stats.begin ();
const currentTime = performance.now ();
const dt = (currentTime - lastTime) / 1000 ;
lastTime = currentTime;
emitter.update (dt);
const vertices = emitter.get_vertices ();
const sizes = emitter.get_sizes ();
if (vertices.length > 0 ) {
particles.geometry .attributes .position .array = new Float32Array (
vertices.slice (0 , vertices.length / 7 * 3 )
);
particles.geometry .attributes .color .array = new Float32Array (
vertices.slice (vertices.length / 7 * 3 )
);
particles.geometry .attributes .size .array = new Float32Array (sizes);
particles.geometry .attributes .position .needsUpdate = true ;
particles.geometry .attributes .color .needsUpdate = true ;
particles.geometry .attributes .size .needsUpdate = true ;
particles.geometry .computeBoundingSphere ();
}
particleCountSpan.textContent = emitter.get_count ();
fpsSpan.textContent = Math .round (1 / dt);
camera.position .x = Math .sin (currentTime * 0.0005 ) * 10 ;
camera.position .z = Math .cos (currentTime * 0.0005 ) * 10 ;
camera.lookAt (0 , 0 , 0 );
renderer.render (scene, camera);
stats.end ();
requestAnimationFrame (animate);
}
animate ();
window .addEventListener ('resize' , () => {
camera.aspect = window .innerWidth / window .innerHeight ;
camera.updateProjectionMatrix ();
renderer.setSize (window .innerWidth , window .innerHeight );
});
}
run ();
4.10 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 测试项目
五、性能优化
5.1 编译器优化 [profile.release]
lto = true
codegen-units = 1
opt-level = 3
5.2 数据布局优化 使用 #[repr(C)] 属性对齐结构体,减少内存对齐开销:
#[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 内存管理优化
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 算法优化
#[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 ;
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 );
let vx = f32x4_splat (particle.vx);
let vy = f32x4_splat (particle.vy);
let vz = f32x4_splat (particle.vz);
let v_vec = f32x4 (particle.vx, particle.vy, particle.vz, 0.0 );
let v_new = v_vec + a_vec * dt_vec;
particle.vx = f32x4_extract_lane (v_new, 0 );
particle.vy = f32x4_extract_lane (v_new, 1 );
particle.vz = f32x4_extract_lane (v_new, 2 );
let x = f32x4_splat (particle.x);
let y = f32x4_splat (particle.y);
let z = f32x4_splat (particle.z);
let pos_vec = f32x4 (particle.x, particle.y, particle.z, 0.0 );
let pos_new = pos_vec + v_new * dt_vec;
particle.x = f32x4_extract_lane (pos_new, 0 );
particle.y = f32x4_extract_lane (pos_new, 1 );
particle.z = f32x4_extract_lane (pos_new, 2 );
particle.lifetime += dt;
particle.a = 1.0 - particle.lifetime / particle.max_lifetime;
particle.size = 0.05 * (1.0 - particle.lifetime / particle.max_lifetime);
}
}
}
}
5.5 Web Workers 并行计算 使用 Web Workers 将粒子更新任务并行化:
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.z = data.z ;
if (data.maxParticles !== maxParticles) {
maxParticles = data.maxParticles ;
emitter.max_particles = maxParticles;
}
if (data.emissionRate !== emissionRate) {
emissionRate = data.emissionRate ;
emitter.emission_rate = emissionRate;
}
break ;
case 'reset' :
emitter.x = 0 ;
emitter.y = 0 ;
emitter.z = 0 ;
emitter.max_particles = maxParticles;
emitter.emission_rate = emissionRate;
self.postMessage ({ type : 'reset' });
break ;
}
};
const worker = new Worker ('worker.js' , { type : 'module' });
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 ,
},
});
worker.onmessage = (e ) => {
const { type, data } = e.data ;
switch (type) {
case 'update' :
const vertices = data.vertices ;
const sizes = data.sizes ;
if (vertices.length > 0 ) {
particles.geometry .attributes .position .array = new Float32Array (
vertices.slice (0 , vertices.length / 7 * 3 )
);
particles.geometry .attributes .color .array = new Float32Array (
vertices.slice (vertices.length / 7 * 3 )
);
particles.geometry .attributes .size .array = new Float32Array (sizes);
particles.geometry .attributes .position .needsUpdate = true ;
particles.geometry .attributes .color .needsUpdate = true ;
particles.geometry .attributes .size .needsUpdate = true ;
particles.geometry .computeBoundingSphere ();
}
particleCountSpan.textContent = data.count ;
break ;
case 'reset' :
particles.geometry .setAttribute ('position' , new THREE .Float32BufferAttribute ([], 3 ));
particles.geometry .setAttribute ('color' , new THREE .Float32BufferAttribute ([], 4 ));
particles.geometry .setAttribute ('size' , new THREE .Float32BufferAttribute ([], 1 ));
break ;
}
};
六、项目部署
6.1 使用 Vite 打包 npm init -y
npm install -D vite
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 ,
},
});
{
"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 [build]
base = "static"
publish = "dist"
command = "npm run build"
在 Netlify 上创建项目,连接到 GitHub 仓库,部署项目。
6.3 部署到 Vercel {
"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 数据可视化应用。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online