经纬志
首页 / luma.gl / 教程

画布、尺寸与设备像素比

先看结论

画布有两套尺寸:CSS 尺寸决定它在页面里占多大,绘制缓冲尺寸决定真正画多少像素。 两者之比就是设备像素比。luma.gl 的设备把这两件事绑在一起, 弄错任何一侧都会得到模糊或错位的画面。

最后验证 2026-11-03 · luma.gl 9.4 · 更新 2026-10-29 · 引用本文

画布上下文的三个参数

createCanvasContext 在创建设备时传入,负责绑定画布、 设置像素比与自动 resize。

const device = await luma.luma.createDevice({
  type: 'webgl',
  adapters: [luma.webgl2Adapter],
  createCanvasContext: {
    canvas: document.getElementById('cv'),
    useDevicePixels: Math.min(window.devicePixelRatio, 2),
    autoResize: true
  }
});
参数取值说明
canvas元素或 id省略时由 true 自动新建一张
useDevicePixelstrue / false / 数字数字即上限,常用 Math.min(dpr, 2)
autoResizetrue / false容器尺寸变化时自动同步绘制缓冲

useDevicePixels: true 在高分屏上会把绘制缓冲放大到 3 倍甚至 4 倍, 像素量随之翻几倍。视觉收益在 2 倍之后基本饱和,性能却持续下降, 所以截到 2 是通用做法。

CSS 尺寸必须自己写

luma.gl 只调整绘制缓冲,不会给画布加样式。画布的 CSS 尺寸为 0 时, 无论像素比是多少,画面上都看不到东西。

html, body { height: 100%; margin: 0; }

#cv {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;        /* 触摸设备上不让页面抢走手势 */
}

常见错误是只给 width 属性、不给 CSS 宽度:浏览器按 width 的像素值显示,容器一变宽,画布就被拉伸变形。

逐帧拿到真实尺寸

AnimationLoop 的回调里,width 与 height 是已经按像素比换算后的绘制尺寸,可以直接用来构造投影矩阵。

const loop = new luma.AnimationLoop({
  device: device,
  onRender: function (props) {
    const aspect = props.width / props.height;

    const pass = props.device.beginRenderPass({
      clearColor: [0.05, 0.07, 0.10, 1]
    });
    model.draw(pass);
    pass.end();

    if (props.tick % 60 === 0) {
      console.log('绘制尺寸', props.width, props.height, '宽高比', aspect.toFixed(3));
    }
  }
});

await loop.start();

用 canvas.clientWidth 直接算投影,会得到 CSS 像素而不是设备像素, 在高分屏上投影比例与实际视口差一倍以上,物体会被压扁。

尺寸变化时的处理

autoResize: true 覆盖了容器尺寸变化的情况,但离屏渲染目标 不会跟着变。窗口改变后,需要重建与尺寸相关的纹理与帧缓冲。

let offscreenSize = 0;
let fbo = null;

function ensureOffscreen(props) {
  const size = Math.max(1, Math.floor(props.height * 0.5));
  if (size === offscreenSize) return;

  offscreenSize = size;
  if (fbo) fbo.destroy();

  const color = device.createTexture({
    width: size,
    height: size,
    format: 'rgba8unorm',
    usage: luma.Texture.SAMPLE | luma.Texture.RENDER
  });

  fbo = device.createFramebuffer({
    width: size,
    height: size,
    colorAttachments: [color]
  });
}

先销毁旧帧缓冲再建新的,否则每次窗口拖动都会泄漏一组纹理。 尺寸取整是必要的:纹理宽高必须是整数。

注意事项

把绘制缓冲尺寸当 CSS 尺寸用

鼠标事件给的是 CSS 像素坐标,拾取时却要在绘制缓冲里定位。 两者相差一个像素比,未换算时点击位置与命中结果偏到画面外。 用 event.clientX * devicePixelRatio 一类换算补齐。

WebGL 后端只挂一个画布

同一台设备再调用 createCanvasContext 会抛错。 需要第二个视图时,用帧缓冲渲染再合成,或者复制像素到另一张 2D 画布; WebGPU 后端才支持同时挂多个画布。

容器隐藏时尺寸为 0

标签页切换、抽屉收起时容器宽高变成 0,绘制缓冲被压到 1×1。 恢复显示后必须再触发一次 resize,否则画面停在 1 像素的状态。

引用本文

经纬志(2026)。《画布、尺寸与设备像素比》。https://www.jingweizhi.com/lumagl/tutorial/canvas-sizing.html(访问日期 2026-11-03)。