跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Rust大前端算法

Rust WebAssembly 与 Three.js 结合的高性能 3D 粒子系统实战

介绍如何利用 Rust WebAssembly 与 Three.js 构建高性能 3D 粒子系统。通过 wasm-bindgen 实现 Rust 与 JavaScript 的交互,传递顶点、纹理及变换矩阵数据。涵盖项目结构搭建、粒子生命周期管理、物理模拟及动画效果实现。重点讲解编译器优化、SIMD 指令加速及 Web Workers 并行计算等性能提升方案,并包含 Vite 打包与 Netlify/Vercel 部署流程,适用于百万级粒子渲染场景。

花里胡哨发布于 2026/4/6更新于 2026/5/2028 浏览
Rust WebAssembly 与 Three.js 结合的高性能 3D 粒子系统实战

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 的渲染流程:

  1. 顶点着色器:对每个顶点进行变换,计算其在屏幕上的位置
  2. 图元装配:将顶点连接成图元(如点、线、三角形)
  3. 光栅化:将图元转换为像素
  4. 片段着色器:对每个像素进行颜色计算和纹理采样
  5. 测试与混合:对像素进行深度测试、模板测试和颜色混合,最终输出到屏幕
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);

// 创建材质
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。顶点数据通常包括位置、颜色、纹理坐标等。

首先,在 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 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);

  // 从 Rust Wasm 获取顶点数据
  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。纹理数据通常是一个二维数组,每个元素代表一个像素的颜色。

首先,在 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.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)
  );

  // 从 Rust Wasm 获取纹理数据
  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。变换矩阵通常包括平移矩阵、旋转矩阵、缩放矩阵、透视矩阵等。

首先,在 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) -> 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);

  // 从 Rust Wasm 获取模型矩阵
  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
// 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
// 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
// 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) -> 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
// 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);

  // 创建相机
  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);

  // 创建 Rust Wasm 粒子发射器
  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
<!-- 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);

                // 计算速度
                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 将粒子更新任务并行化:

// 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.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;
  }
};

然后,在主进程中使用 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) => {
  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 打包

安装 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 数据可视化应用。

目录

  1. Rust WebAssembly 与 Three.js 结合的高性能 3D 粒子系统实战
  2. 一、引言
  3. 二、WebGL 与 Three.js 基础
  4. 2.1 WebGL 概述
  5. 2.2 Three.js 概述
  6. 2.3 Three.js 的安装与使用
  7. 使用 npm 安装
  8. 三、Rust WebAssembly 与 WebGL 交互
  9. 3.1 传递顶点数据
  10. 3.2 传递纹理数据
  11. 3.3 传递变换矩阵
  12. 四、实战项目:高性能粒子系统
  13. 4.1 项目概述
  14. 4.2 项目结构
  15. 4.3 Cargo.toml
  16. 4.4 src/particle.rs
  17. 4.5 src/emitter.rs
  18. 4.6 src/utils.rs
  19. 4.7 src/lib.rs
  20. 4.8 static/style.css
  21. 4.9 static/main.js
  22. 4.10 static/index.html
  23. 4.11 编译项目
  24. 4.12 测试项目
  25. 五、性能优化
  26. 5.1 编译器优化
  27. 5.2 数据布局优化
  28. 5.3 内存管理优化
  29. 5.4 算法优化
  30. 5.5 Web Workers 并行计算
  31. 六、项目部署
  32. 6.1 使用 Vite 打包
  33. 6.2 部署到 Netlify
  34. 6.3 部署到 Vercel
  35. 七、总结
  36. 7.1 技术栈
  37. 7.2 核心功能
  38. 7.3 未来改进
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 理解 Python 异步编程原理与 asyncio 简单实现
  • RAG 技术演进:DeepSeek 与 Neo4j 构建 GraphRAG 企业智能体系
  • 本地离线部署大模型 Ollama+AnythingLLM
  • OpenClaw 安装部署全流程:搭建自托管 AI 助手网关
  • AI 编程助手横向评测:GitHub Copilot vs CodeWhisperer vs Cursor
  • Raphael AI:基于 Flux 模型的免费 AI 图像生成工具
  • 大模型微调常见方法总结
  • OpenClaw 飞书机器人配置指南:实现群消息免@自动回复
  • GLM-OCR:基于 GLM-V 架构的多模态 OCR 模型
  • Python 零基础入门到进阶学习路径详解
  • HarmonyOS RcList 组件缩略图、角标与图标系统设计
  • Ubuntu 18.04 在 VMware 中的安装教程
  • Git 远程仓库连接实战:HTTPS 与 SSH 配置详解
  • AI 变现深度拆解:为何掌握大量工具仍无法盈利
  • Python 实现 MCP 客户端调用高德地图天气查询示例
  • OpenClaw 本地极简部署与 QQ 机器人接入教程
  • Copilot/Codex 中文乱码修复与自动化配置方案
  • 利用闲置腾讯云服务器部署 Openclaw 并接入飞书
  • Rspack:基于 Rust 的下一代高性能 Web 构建工具
  • Stable Diffusion 模型加载报错:CheckpointLoaderSimple 错误修复

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如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