跳到主要内容Rust WebAssembly 与 Three.js 结合的高性能粒子系统实战 | 极客日志Rust大前端算法
Rust WebAssembly 与 Three.js 结合的高性能粒子系统实战
Rust WebAssembly 结合 Three.js 实现高性能 3D 粒子系统。通过 wasm-bindgen 交互,利用 Rust 内存安全特性优化百万级粒子渲染。涵盖顶点数据传递、纹理生成、变换矩阵计算及 SIMD 并行优化。项目包含完整源码结构、Vite 打包部署流程,解决传统 JS 方案在大数据量下的性能瓶颈。
颠三倒四17 浏览 Rust WebAssembly 与 Three.js 结合的高性能粒子系统实战

引言
3D 数据可视化在现代 Web 应用中扮演着重要角色,无论是数据分析、科学计算还是虚拟仿真,对渲染性能的要求都越来越高。传统的 JavaScript + WebGL/Three.js 方案在处理百万级粒子时,往往受限于单线程和 GC 机制,难以维持高帧率。
Rust 凭借其内存安全和零成本抽象的特性,配合 WebAssembly (Wasm),非常适合承担核心计算任务。本文将深入探讨如何利用 Rust Wasm 优化 Three.js 的粒子系统,实现高性能的创建、更新与渲染,并分享实际项目中的优化经验。
WebGL 与 Three.js 基础
WebGL 概述
WebGL 基于 OpenGL ES,允许浏览器直接调用 GPU 进行 3D 渲染。其核心在于 GLSL 着色器语言:
- 顶点着色器:处理顶点变换,计算屏幕坐标。
- 片段着色器:处理像素颜色与纹理采样。
渲染流程大致为:顶点着色 -> 图元装配 -> 光栅化 -> 片段着色 -> 混合输出。
Three.js 核心组件
Three.js 封装了 WebGL 的复杂性,提供了更高级的抽象:
- 场景 (Scene):3D 空间容器。
- 相机 (Camera):决定观察视角(透视或正交)。
- 渲染器 (Renderer):将场景绘制到 Canvas。
- 网格 (Mesh):几何体与材质的组合。
- 光源 (Light):环境光、方向光等。
快速上手
安装 Three.js:
npm install three
创建一个基础场景示例:
import * as THREE from 'three';
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
const camera = new .(
,
. / .,
,
);
camera.. = ;
renderer = .({ : });
renderer.(., .);
..(renderer.);
geometry = .(, , );
material = .({ : , : });
cube = .(geometry, material);
scene.(cube);
ambientLight = .();
scene.(ambientLight);
directionalLight = .(, );
directionalLight..(, , );
scene.(directionalLight);
() {
(animate);
cube.. += ;
cube.. += ;
renderer.(scene, camera);
}
();
.(, {
camera. = . / .;
camera.();
renderer.(., .);
});
THREE
PerspectiveCamera
75
window
innerWidth
window
innerHeight
0.1
1000
position
z
5
const
new
THREE
WebGLRenderer
antialias
true
setSize
window
innerWidth
window
innerHeight
document
body
appendChild
domElement
const
new
THREE
BoxGeometry
1
1
1
const
new
THREE
MeshPhongMaterial
color
0x00ff00
shininess
100
const
new
THREE
Mesh
add
const
new
THREE
AmbientLight
0x404040
add
const
new
THREE
DirectionalLight
0xffffff
0.8
position
set
1
1
1
add
function
animate
requestAnimationFrame
rotation
x
0.01
rotation
y
0.01
render
animate
window
addEventListener
'resize'
() =>
aspect
window
innerWidth
window
innerHeight
updateProjectionMatrix
setSize
window
innerWidth
window
innerHeight
Rust WebAssembly 与 WebGL 交互
传递顶点数据
在 Rust 中定义顶点结构并通过 wasm-bindgen 导出,是数据交互的关键。
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 中加载这些数据并构建 BufferGeometry:
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();
传递纹理数据
Rust 可以生成动态纹理数据,例如渐变或噪声图,传递给 Three.js 使用。
#[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
}
JS 端加载逻辑类似,注意使用 DataTexture:
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,
});
传递变换矩阵
对于复杂的模型变换,可以在 Rust 中计算矩阵并传给 JS,避免频繁在 JS 层做浮点运算。
#[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(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()
}
实战项目:高性能粒子系统
项目架构
我们将构建一个支持百万级粒子的系统,包含发射器、粒子生命周期管理及物理模拟。
particle-system/
├── Cargo.toml
├── src/
│ ├── lib.rs
│ ├── particle.rs
│ ├── emitter.rs
│ └── utils.rs
├── static/
│ ├── index.html
│ ├── style.css
│ └── main.js
└── README.md
核心模块实现
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"] }
rand = "0.8"
serde = { version = "1.0", features = ["derive"] }
粒子结构 (src/particle.rs)
使用 #[repr(C)] 确保内存布局连续,利于 SIMD 优化。
use rand::Rng;
use serde::Serialize;
#[derive(Debug, Clone, Copy, Serialize)]
#[repr(C)]
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
}
}
发射器 (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
}
}
入口文件 (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();
}
前端集成
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;
}
主逻辑 (static/main.js)
这里我们使用 ShaderMaterial 来增强粒子效果,并集成 Stats 监控性能。
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);
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();
}
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();
编译与测试
wasm-pack build --target web
npx serve .
性能优化
编译器优化
在 Cargo.toml 中启用 LTO 和全量优化:
[profile.release]
lto = true
codegen-units = 1
opt-level = 3
数据布局优化
使用 #[repr(C)] 保证结构体内存对齐,减少缓存未命中。
算法优化:SIMD
利用 WASM 的 SIMD 指令批量处理粒子更新,大幅提升吞吐量。
#[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 {
}
}
}
}
Web Workers 并行计算
将粒子更新逻辑移至 Web Worker,避免阻塞主线程渲染。
import init, { EmitterWrapper, init_panic_hook } from '../pkg/particle_system.js';
let emitter;
self.onmessage = async (e) => {
const { type, data } = e.data;
switch (type) {
case 'init':
await init();
init_panic_hook();
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();
self.postMessage({
type: 'update',
data: { vertices, sizes, count: emitter.get_count() },
});
break;
}
};
项目部署
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 },
});
静态托管
可使用 Netlify 或 Vercel 直接部署 dist 目录,配置 netlify.toml 或 vercel.json 即可一键上线。
总结
通过 Rust WebAssembly 与 Three.js 的结合,我们成功构建了一个支持百万级粒子的高性能 3D 可视化系统。Rust 保证了内存安全与计算效率,Three.js 提供了便捷的渲染接口,两者互补解决了传统 JS 方案的性能瓶颈。
- 掌握 Rust Wasm 与 WebGL 的数据交互模式。
- 理解粒子系统的物理模拟与生命周期管理。
- 学会通过编译器优化、SIMD 及多线程进一步提升性能。
未来可进一步探索 WebGPU 支持、物理引擎集成以及更丰富的用户交互界面。希望这篇实战指南能为你的 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