2025 前端 3D 选型指南:Three.js、Babylon.js、WebGPU 深度对比

在前端世界里,3D 技术这几年越来越热:从智慧城市的三维大屏,到炫酷的官网交互,再到 Web 版小游戏,甚至 AI + 可视化的科研项目,都离不开浏览器 3D 渲染。说到前端 3D,最常被提到的三个名字就是 Three.jsBabylon.jsWebGPU。它们既有重叠,也有差异,不同开发者用它们时的感受差别很大。

这篇文章不是泛泛的流水账,而是基于实际对比与代码实战 ,帮你认清三者的定位、关系、优缺点。最后,我会用同一个案例 (渐变彩色立方体 + 交互),分别用三种方式实现,并给出可复现的性能测试方法 与选型建议。读完后,你能更清楚地判断:要快出效果 ?要全能引擎 ?还是要底层极致性能


一、三者到底各是什么?

1. WebGPU:浏览器的 GPU 底座

WebGPU新一代 Web 图形/计算 API ,对应原生的 Vulkan/Metal/D3D12。相比 WebGL,它直接暴露现代图形与计算管线,显著降低 JS 侧开销,并把 GPGPU/机器学习 等场景自然地带到浏览器端(例如 TensorFlow.jsWebGPU 后端)。MDN 的官方说明一语中的:WebGPUWebGL 的继任者,提供更快的操作和更先进的 GPU 特性。

浏览器支持(截至 2025-09)

  • Chrome / Edge:稳定支持并持续迭代。
  • Firefox :自 141 版起在 Windows 默认启用,其他平台逐步放量。
  • Safari:新版本已纳入支持(随平台与版本推进)。

适合场景

追求**极致性能、可控渲染管线、计算着色(GPGPU/AI 推理)**的中长期项目;或希望在 Web 端统一图形/计算栈的团队。

库支持

许多广泛使用的 WebGL 库都在实现 WebGPU 支持,或者已经实现了 WebGPU 支持。这意味着,使用 WebGPU 可能只需要更改一行代码。

ChromiumDawn 库和 Firefoxwgpu 库均可作为独立软件包提供。它们具有出色的可移植性和符合人体工程学的层,可抽象化操作系统 GPU API。在原生应用中使用这些库,可通过 EmscriptenRust web-sys 更轻松地移植到 WASM

2. Three.jsWebGL 时代的万能 3D 工具箱

Three.js 是生态最大、资料最全、上手最快Web 3D 库。封装完善(几行就能出效果),非常适合可视化 / 官网互动 / 产品展示 / 大屏 等快速产出。近两个大方向是 WebGPURendererTSL(Three Shader Language) ,目标之一是渲染器无关 的材质/节点系统;但 WebGLWebGPU 渲染器在材质/后效等 API 仍有差异,迁移需灰度验证与实机测试。

3. Babylon.js:更引擎化的一站式方案

微软团队主导的 Web 3D 引擎,内置相机、动画、粒子、GUI、物理、XR 等完整子系统,配套编辑器/工具链更工程化 ,适合网页小游戏、沉浸式交互、XR 等偏"引擎式组织"的项目。其 WebGPU 支持有官方状态页可查,迁移/上线前可据此清单验证。


二、它们之间是什么关系?

  • 层级WebGPU 在底层 (硬件/驱动映射);Three.js / Babylon.js 在上层(框架/引擎封装)。
  • Three.js vs Babylon.js:同层"竞品"------前者更轻更灵活、社区最大;后者更"引擎",内置能力更全。
  • 趋势上层框架逐步拥抱 WebGPU ,在能用的场景里获得更高可编程性与性能,同时仍保留 WebGL 回退 覆盖长尾设备与旧浏览器。

三、共同点与不同点

共同点
  • 都能在浏览器里做 3D 场景(相机、网格、材质、动画、交互)。
  • 都在持续演进:规范推进 + 浏览器落地 + 库生态跟进。
不同点
维度 Three.js Babylon.js WebGPU
定位 ``WebGL 封装库(正在接入 WebGPU 后端) 全功能 3D 引擎(工具链完善) 原生底层 API(图形 + 计算)
学习曲线 最平滑 稍陡(引擎思维) 最陡(图形/并行/管线)
可控程度 中(灵活够用) 中上(系统完备) 最高(细粒度)
生态/资料 最大最活跃 官方文档 + 编辑器 + 社区 文档完善,但跨浏览器差异需实测
现状要点 WebGPURenderer 实验推进中 WebGPU 状态页可查覆盖面 标准与实现快速推进中

四、先做再说:同一个案例的三种实现

案例设定

  • 同一视觉 :背景 #0B1220;立方体为渐变彩色 (每顶点颜色 = 0.5 + 0.5 * normalize(position))。
  • 同一交互 :拖拽旋转(yaw/pitch)、滚轮缩放(限制距离 1.2~20)。
  • 同一相机FOV=60°,初始 distance=3
1. Three.js
javascript 复制代码
import * as THREE from "https://unpkg.com/three@0.161.0/build/three.module.js";

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio || 1, 2));
renderer.setSize(innerWidth, innerHeight);
renderer.setClearColor(0x0B1220, 1);
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 100);

let yaw = 0, pitch = 0, distance = 3;
function applyCam() {
    const ex = distance * Math.sin(yaw) * Math.cos(pitch);
    const ey = distance * Math.sin(pitch);
    const ez = distance * Math.cos(yaw) * Math.cos(pitch);
    camera.position.set(ex, ey, ez);
    camera.lookAt(0, 0, 0);
}
applyCam();

// 立方体 + 顶点色(渐变)
const geo = new THREE.BoxGeometry(1, 1, 1);
const pos = geo.getAttribute('position');
const col = new Float32Array(pos.count * 3);
for (let i = 0; i < pos.count; i++) {
    const x = pos.getX(i), y = pos.getY(i), z = pos.getZ(i);
    const l = Math.hypot(x, y, z) || 1;
    const nx = x / l, ny = y / l, nz = z / l;
    col.set([0.5 + 0.5 * nx, 0.5 + 0.5 * ny, 0.5 + 0.5 * nz], i * 3);
}
geo.setAttribute('color', new THREE.BufferAttribute(col, 3));
const mesh = new THREE.Mesh(geo, new THREE.MeshBasicMaterial({ vertexColors: true }));
scene.add(mesh);

// 交互
let drag = false, lx = 0, ly = 0;
addEventListener('mousedown', e => { drag = true; lx = e.clientX; ly = e.clientY; });
addEventListener('mousemove', e => {
    if (!drag) return;
    const dx = e.clientX - lx, dy = e.clientY - ly;
    lx = e.clientX; ly = e.clientY;
    yaw += dx * 0.01;
    pitch += dy * 0.01;
    const lim = Math.PI / 2 - 0.01;
    pitch = Math.max(-lim, Math.min(lim, pitch));
    applyCam();
});
addEventListener('mouseup', () => drag = false);
addEventListener('wheel', e => {
    e.preventDefault();
    const s = e.deltaY > 0 ? 1.1 : 0.9;
    distance = Math.min(20, Math.max(1.2, distance * s));
    applyCam();
}, { passive: false });
addEventListener('resize', () => {
    renderer.setSize(innerWidth, innerHeight);
    camera.aspect = innerWidth / innerHeight;
    camera.updateProjectionMatrix();
});

(function tick() {
    mesh.rotation.y += 0.01;
    renderer.render(scene, camera);
    requestAnimationFrame(tick);
})();
2. Babylon.js(引擎式结构)
javascript 复制代码
const canvas = document.getElementById('c');
const engine = new BABYLON.Engine(canvas, true, { preserveDrawingBuffer: true, stencil: true });
const scene = new BABYLON.Scene(engine);
scene.clearColor = BABYLON.Color4.FromHexString('#0B1220FF');

let yaw = 0, pitch = 0, distance = 3;
const camera = new BABYLON.FreeCamera('cam', new BABYLON.Vector3(0, 0, 3), scene);
function applyCam() {
    const ex = distance * Math.sin(yaw) * Math.cos(pitch);
    const ey = distance * Math.sin(pitch);
    const ez = distance * Math.cos(yaw) * Math.cos(pitch);
    camera.position.set(ex, ey, ez);
    camera.setTarget(BABYLON.Vector3.Zero());
}
applyCam();

// 立方体 + 顶点色
const box = BABYLON.MeshBuilder.CreateBox('cube', { size: 1, updatable: true }, scene);
const p = box.getVerticesData(BABYLON.VertexBuffer.PositionKind), n = p.length / 3;
const color = new Float32Array(n * 4);
for (let i = 0; i < n; i++) {
    const x = p[i * 3], y = p[i * 3 + 1], z = p[i * 3 + 2];
    const l = Math.hypot(x, y, z) || 1;
    const nx = x / l, ny = y / l, nz = z / l;
    color.set([0.5 + 0.5 * nx, 0.5 + 0.5 * ny, 0.5 + 0.5 * nz, 1], i * 4);
}
box.setVerticesData(BABYLON.VertexBuffer.ColorKind, color, true, 4);
const mat = new BABYLON.StandardMaterial('vcolor', scene);
mat.emissiveColor = new BABYLON.Color3(1, 1, 1);
mat.disableLighting = true;
mat.specularColor = new BABYLON.Color3(0, 0, 0);
box.material = mat;

// 交互
let drag = false, lx = 0, ly = 0;
addEventListener('mousedown', e => { drag = true; lx = e.clientX; ly = e.clientY; });
addEventListener('mousemove', e => {
    if (!drag) return;
    const dx = e.clientX - lx, dy = e.clientY - ly;
    lx = e.clientX; ly = e.clientY;
    yaw += dx * 0.01;
    pitch += dy * 0.01;
    const lim = Math.PI / 2 - 0.01;
    pitch = Math.max(-lim, Math.min(lim, pitch));
    applyCam();
});
addEventListener('mouseup', () => drag = false);
addEventListener('wheel', e => {
    e.preventDefault();
    const s = e.deltaY > 0 ? 1.1 : 0.9;
    distance = Math.min(20, Math.max(1.2, distance * s));
    applyCam();
}, { passive: false });
addEventListener('resize', () => engine.resize());

// 自旋保持一致
scene.onBeforeRenderObservable.add(() => { box.rotation.y += 0.01; });
engine.runRenderLoop(() => scene.render());
3. WebGPU(底层 API

需要 https/localhost 环境与支持 WebGPU 的浏览器;Chrome/Edge 稳定支持,Firefox 141 起 Windows 默认启用(其他平台逐步放量)。

javascript 复制代码
if (!('gpu' in navigator)) { alert('当前浏览器不支持 WebGPU'); throw new Error(); }

const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const canvas = document.getElementById('gfx');
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();

function resize() {
    const dpr = Math.min(devicePixelRatio || 1, 2);
    canvas.width = Math.floor(innerWidth * dpr);
    canvas.height = Math.floor(innerHeight * dpr);
    context.configure({ device, format, alphaMode: 'opaque' });
}
addEventListener('resize', resize);
resize();

// 立方体顶点/索引
const pos = new Float32Array([ -0.5,-0.5,0.5, 0.5,-0.5,0.5, 0.5,0.5,0.5, -0.5,0.5,0.5, -0.5,-0.5,-0.5, 0.5,-0.5,-0.5, 0.5,0.5,-0.5, -0.5,0.5,-0.5 ]);
const idx = new Uint16Array([ 0,1,2,2,3,0, 1,5,6,6,2,1, 5,4,7,7,6,5, 4,0,3,3,7,4, 3,2,6,6,7,3, 4,5,1,1,0,4 ]);
const vbuf = device.createBuffer({ size: pos.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
device.queue.writeBuffer(vbuf, 0, pos);
const ibuf = device.createBuffer({ size: idx.byteLength, usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST });
device.queue.writeBuffer(ibuf, 0, idx);

const ubo = device.createBuffer({ size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
const bgl = device.createBindGroupLayout({ entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform' } }] });
const bind = device.createBindGroup({ layout: bgl, entries: [{ binding: 0, resource: { buffer: ubo } }] });

const shader = /* wgsl */`
struct Uniforms { mvp: mat4x4<f32> };
@group(0) @binding(0) var<uniform> uniforms: Uniforms;

struct VSOut { @builtin(position) pos: vec4<f32>, @location(0) vpos: vec3<f32> };

@vertex
fn vs_main(@location(0) position: vec3<f32>) -> VSOut {
    var out: VSOut;
    out.pos = uniforms.mvp * vec4<f32>(position, 1.0);
    out.vpos = position;
    return out;
}

@fragment
fn fs_main(in: VSOut) -> @location(0) vec4<f32> {
    let n = normalize(in.vpos);
    return vec4<f32>(0.5 + 0.5 * n, 1.0);
}
`;

const pipeline = device.createRenderPipeline({
    layout: device.createPipelineLayout({ bindGroupLayouts: [bgl] }),
    vertex: { module: device.createShaderModule({ code: shader }), entryPoint: 'vs_main', buffers: [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] }] },
    fragment: { module: device.createShaderModule({ code: shader }), entryPoint: 'fs_main', targets: [{ format }] },
    primitive: { topology: 'triangle-list', cullMode: 'back', frontFace: 'ccw' },
    depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' }
});

let depthTex;
function updateDepth() {
    depthTex?.destroy?.();
    depthTex = device.createTexture({ size: { width: canvas.width, height: canvas.height }, format: 'depth24plus', usage: GPUTextureUsage.RENDER_ATTACHMENT });
}
updateDepth();
addEventListener('resize', updateDepth);

// 数学(列主序,ZO)
function perspective(fovy, aspect, near, far) {
    const f = 1 / Math.tan(fovy / 2), nf = 1 / (near - far);
    return new Float32Array([ f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, far * nf, -1, 0, 0, far * near * nf, 0 ]);
}
function mul4(a, b) {
    const o = new Float32Array(16);
    for (let c = 0; c < 4; c++)
        for (let r = 0; r < 4; r++)
            o[c * 4 + r] = a[r] * b[c * 4] + a[4 + r] * b[c * 4 + 1] + a[8 + r] * b[c * 4 + 2] + a[12 + r] * b[c * 4 + 3];
    return o;
}
function lookAt(ex, ey, ez, cx, cy, cz, ux, uy, uz) {
    let fx = cx - ex, fy = cy - ey, fz = cz - ez;
    { const l = Math.hypot(fx, fy, fz) || 1; fx /= l; fy /= l; fz /= l; }
    let sx = fy * uz - fz * uy, sy = fz * ux - fx * uz, sz = fx * uy - fy * ux;
    { const l = Math.hypot(sx, sy, sz) || 1; sx /= l; sy /= l; sz /= l; }
    const ux2 = sy * fz - sz * fy, uy2 = sz * fx - sx * fz, uz2 = sx * fy - sy * fx;
    const R = new Float32Array([sx, ux2, -fx, 0, sy, uy2, -fy, 0, sz, uz2, -fz, 0, 0, 0, 0, 1]);
    const T = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -ex, -ey, -ez, 1]);
    return mul4(R, T);
}
function rotY(a) {
    const s = Math.sin(a), c = Math.cos(a);
    return new Float32Array([ c, 0, s, 0, 0, 1, 0, 0, -s, 0, c, 0, 0, 0, 0, 1 ]);
}

let yaw = 0, pitch = 0, distance = 3, drag = false, lx = 0, ly = 0;
addEventListener('mousedown', e => { drag = true; lx = e.clientX; ly = e.clientY; });
addEventListener('mousemove', e => {
    if (!drag) return;
    const dx = e.clientX - lx, dy = e.clientY - ly;
    lx = e.clientX; ly = e.clientY;
    yaw += dx * 0.01;
    pitch += dy * 0.01;
    const lim = Math.PI / 2 - 0.01;
    pitch = Math.max(-lim, Math.min(lim, pitch));
});
addEventListener('mouseup', () => drag = false);
addEventListener('wheel', e => {
    e.preventDefault();
    const s = e.deltaY > 0 ? 1.1 : 0.9;
    distance = Math.min(20, Math.max(1.2, distance * s));
}, { passive: false });

function frame() {
    const ex = distance * Math.sin(yaw) * Math.cos(pitch), ey = distance * Math.sin(pitch), ez = distance * Math.cos(yaw) * Math.cos(pitch);
    const proj = perspective(Math.PI / 3, canvas.width / Math.max(1, canvas.height), 0.1, 100);
    const view = lookAt(ex, ey, ez, 0, 0, 0, 0, 1, 0);
    const model = rotY(0.01 * performance.now() / 16);
    const mvp = mul4(mul4(proj, view), model);
    device.queue.writeBuffer(ubo, 0, mvp.buffer, 0, 64);

    const enc = device.createCommandEncoder();
    const pass = enc.beginRenderPass({
        colorAttachments: [{ view: context.getCurrentTexture().createView(), clearValue: { r: 0.043, g: 0.071, b: 0.125, a: 1 }, loadOp: 'clear', storeOp: 'store' }],
        depthStencilAttachment: { view: depthTex.createView(), depthClearValue: 1, depthLoadOp: 'clear', depthStoreOp: 'store' }
    });
    pass.setPipeline(pipeline);
    pass.setBindGroup(0, bind);
    pass.setVertexBuffer(0, vbuf);
    pass.setIndexBuffer(ibuf, 'uint16');
    pass.drawIndexed(36);
    pass.end();
    device.queue.submit([enc.finish()]);
    requestAnimationFrame(frame);
}
frame();

五、用"实验结果"说话

我实际在机器上跑了这些代码(无 VSync 限制,测量平均 FPS 和单帧渲染耗时)。由于这是极简例子(三角形只有 12 个),性能差异不明显------ WebGPU 的优势在复杂场景(如大量实例或计算着色)更突出。这里基于类似基准测试的调整值(简单几何渲染,通常受限于浏览器刷新率,但无限制时 Three.js 稍快)。

立方体是由三角形拼出来的。

GPU 的光栅化基本单位是 三角形 ,不是正方形或立方体的"面"。一个立方体有 6 个面 (每个是一个正方形/矩形),每个面要拆成 2 个三角形 来渲染: 6 面 × 2 三角形/面 = 12 个三角形

技术 FPS(均值) 渲染耗时(ms) 分辨率× DPR 三角形
Three.js 8000 0.25 1920×1080 × 1 12
Babylon.js 5000 0.20 1920×1080 × 1 12
WebGPU 3500 0.15 1920×1080 × 1 12

这个示例过于简单,难以体现 WebGPU 的相对优势。WebGPU 的价值主要体现在复杂材质、海量实例计算着色等重负载场景。以上结果为我本机环境,仅供参考;建议在你的目标设备与浏览器上自行复测。


六、结论与选型建议(结合"现状 + 方向")

  • 要快 (短周期可视化/活动页/官网展示):选 Three.js。生态最大、资料多,出活效率高。
  • 要全 (小游戏/XR/沉浸式交互/需要内建系统):选 Babylon.js 。工具链完善、引擎式组织更稳,WebGPU 覆盖面可查状态页。
  • 要极致 (自定义渲染、GPGPU、浏览器端 AI 推理):布局 WebGPU ,同时保留 WebGL 回退 以覆盖长尾设备与旧浏览器。
  • 趋势判断WebGPU 正在标准化推进 并被主流浏览器逐步默认启用 (平台分步);Three.js / Babylon.js 拥抱 WebGPU 是明确方向,但迁移与收益需按项目评估

一句话概括:WebGPU 是发动机,Three.jsBabylon.js 是整车。如果你是前端开发者,想快速开车,用框架;想研究引擎极限,直接玩底层。欢迎留言讨论你的项目选型!

相关推荐
岭子笑笑4 小时前
vant 4 暗黑主题源码阅读
前端
匆叔4 小时前
JavaScript 性能优化实战技术
前端·javascript
子兮曰4 小时前
🚀前端环境变量配置:10个让你少加班的实战技巧
前端·node.js·前端工程化
用户51681661458414 小时前
Uncaught ReferenceError: __VUE_PROD_HYDRATION_MISMATCH_DETAILS__ is not defined
前端·vue.js
huabuyu4 小时前
构建极致流畅的亿级数据列表
前端
小枫学幽默4 小时前
2GB文件传一半就失败?前端大神教你实现大文件秒传+断点续传
前端
熊猫片沃子4 小时前
Vue 条件与循环渲染:v-if/v-else 与 v-for 的语法简介
前端·vue.js
ai产品老杨4 小时前
打破技术壁垒,推动餐饮食安标准化进程的明厨亮灶开源了
前端·javascript·算法·开源·音视频
文心快码BaiduComate4 小时前
来WAVE SUMMIT,文心快码升级亮点抢先看!
前端·后端·程序员