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 是整车。如果你是前端开发者,想快速开车,用框架;想研究引擎极限,直接玩底层。欢迎留言讨论你的项目选型!

相关推荐
恋猫de小郭1 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端