用 luma.gl 直接写一个 WebGL 程序
不经过 deck.gl,只用 luma.gl 画一个三角形。流程是:创建设备、写缓冲区、 组装模型、开渲染通道、绘制。每一步都对应原生 WebGL 里的一组操作。
完整代码
页面用 UMD 版本,四个包依次加载:core 提供设备与资源,
webgl 提供 WebGL2 适配器,shadertools 提供着色器组装,
engine 提供 Model 与 AnimationLoop。
它们共用一个全局对象 luma,设备的创建入口因此写成
luma.luma.createDevice;ESM 版本直接
import { luma } from '@luma.gl/core' 即可,没有这一层嵌套。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<style>
html, body { height: 100%; margin: 0; background: #0d1117; }
#cv { display: block; width: 100%; height: 100%; }
</style>
</head>
<body>
<canvas id="cv"></canvas>
<script src="https://cdn.jsdelivr.net/npm/@luma.gl/core@9.4.2/dist/dist.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@luma.gl/webgl@9.4.2/dist/dist.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@luma.gl/shadertools@9.4.2/dist/dist.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@luma.gl/engine@9.4.2/dist/dist.min.js"></script>
<script>
(async function () {
const canvas = document.getElementById('cv');
// 1. 建设备:显式传入 WebGL2 适配器
const device = await luma.luma.createDevice({
type: 'webgl',
adapters: [luma.webgl2Adapter],
createCanvasContext: {
canvas: canvas,
useDevicePixels: Math.min(window.devicePixelRatio, 2),
autoResize: true
}
});
// 2. 三个顶点,归一化设备坐标
const positions = device.createBuffer(new Float32Array([
-0.6, -0.45, 0,
0.6, -0.45, 0,
0.0, 0.55, 0
]));
// 3. 组装模型:着色器 + 属性 + 缓冲区布局
const model = new luma.Model(device, {
topology: 'triangle-list',
vertexCount: 3,
attributes: { positions: positions },
bufferLayout: [
{ name: 'positions', stepMode: 'vertex', format: 'float32x3' }
],
vs: [
'#version 300 es',
'in vec3 positions;',
'void main() {',
' gl_Position = vec4(positions, 1.0);',
'}'
].join('\n'),
fs: [
'#version 300 es',
'precision highp float;',
'out vec4 fragColor;',
'void main() {',
' fragColor = vec4(0.35, 0.80, 0.62, 1.0);',
'}'
].join('\n')
});
// 4. 动画循环:每帧开一个渲染通道并绘制
const loop = new luma.AnimationLoop({
device: device,
onRender: function (props) {
const pass = props.device.beginRenderPass({
clearColor: [0.05, 0.07, 0.10, 1]
});
model.draw(pass);
pass.end();
},
onError: function (error) {
console.error(error);
}
});
await loop.start();
})();
</script>
</body>
</html>
与原生 WebGL 的对应关系
| 原生 WebGL | luma.gl | 说明 |
|---|---|---|
canvas.getContext('webgl2') | luma.createDevice() | 设备同时管理上下文与画布尺寸 |
gl.createBuffer / bufferData | device.createBuffer() | 创建时可直接带初始数据 |
gl.createShader / compileShader | device.createShader() | Model 内部完成编译与链接 |
gl.linkProgram、useProgram | Model / RenderPipeline | 管线与着色器布局自动管理 |
gl.vertexAttribPointer | bufferLayout | 用格式字符串声明,不需要手动算字节偏移 |
gl.clear、gl.drawArrays | beginRenderPass + model.draw | 渲染通道负责清屏与绘制范围 |
三个必须做对的地方
一、适配器不能省
v9 按需引入后端,createDevice 必须拿到 webgl2Adapter。
忘记传会得到「No matching device found」,且不会有更多提示。
只用 core 一个包时也会出现同样的错误,因为适配器不在 core 里。
二、bufferLayout 必须与着色器对齐
每个缓冲区都要声明名字、步进模式与格式。名字必须与着色器里的
in 变量一致,格式必须与实际数据匹配:三个 Float32 对应
float32x3。漏写布局时,控制台会提示
「layout for attribute ... not present」,绘制调用返回成功但画面什么都没有。
三、着色器要写 GLSL 300 es
WebGL2 使用 GLSL ES 3.00,第一行必须是 #version 300 es,
片元着色器要显式声明输出变量,例如 out vec4 fragColor。
沿用 WebGL1 的 gl_FragColor 写法会编译失败。
着色器仍然要自己写
luma.gl 省掉的是状态管理与资源管理,不是着色器逻辑。三角形的顶点变换、
颜色计算、光照模型,依然写在 vs 与 fs 里。
真正的生产力来自它提供的着色器模块与组装能力:把常用的光照、拾取、
坐标变换拆成模块,按需拼进最终着色器。
更新数据
顶点变化时,先改 CPU 侧的数组,再调用 buffer.write(data)。
示例「缓冲区数据更新」用一条波浪线演示了这个流程:每帧重算 200 个点,
整体覆盖缓冲区,绘制拓扑设为 line-strip。
UMD 路径要注意。v9 的 @luma.gl/core 包内,
UMD 文件位于 dist/dist.min.js,不是包根的 dist.min.js。
路径写错会得到 404,页面只剩一块空白画布。
经纬志(2026)。《用 luma.gl 直接写一个 WebGL 程序》。https://www.jingweizhi.com/lumagl/tutorial/raw-webgl.html(访问日期 2026-11-03)。