跳到主要内容

与2D叠层

同一 <canvas> 第一次 getContext 定死模式。已经 webgl2 的元素再 getContext("2d") 得到 null,反过来也一样。2D 笔迹 / 量尺 / 字幕必须 另一张 canvas 或 DOM,两套表面共享 CPU 世界和同一相机矩阵。不要每帧把 2D UI texImage2D 进 GL。

1. 场景怎么切

CPU 世界 + Camera2D(唯一)
├─ GL canvas:场景 mesh / 散点 / 视频板
├─ 2D canvas:笔迹、量尺、临时框
└─ DOM:按钮、IME、tooltip
命中:从上往下(DOM → 2D → GL)
export type OverlayStack = {
glCanvas: HTMLCanvasElement;
inkCanvas: HTMLCanvasElement;
gl: WebGL2RenderingContext;
ink: CanvasRenderingContext2D;
};

export function createOverlayStack(
host: HTMLElement,
glCanvas: HTMLCanvasElement,
inkCanvas: HTMLCanvasElement,
): OverlayStack {
const gl = glCanvas.getContext("webgl2", { preserveDrawingBuffer: false, alpha: true });
const ink = inkCanvas.getContext("2d", { alpha: true });
if (!gl || !ink) {
throw new Error("need two canvases: webgl2 and 2d cannot share one element");
}
host.style.position = "relative";
for (const el of [glCanvas, inkCanvas]) {
el.style.position = "absolute";
el.style.inset = "0";
el.style.width = "100%";
el.style.height = "100%";
}
inkCanvas.style.pointerEvents = "none";
return { glCanvas, inkCanvas, gl, ink };
}

两张 CSS 盒必须重合。drawingBuffer 各自按 DPR 设,见 DPR。2D 用户空间 Y 向下、GL NDC Y 向上:世界点映到 CSS 只用 同一 worldToCss,禁止 GL 一条、2D 一条,见 指针到 NDCCanvas Overlay

适用:GL 场景 + 矢量笔、量尺、临时框选;字幕烧在预览上但不进 shader。
不适用:滤镜必须进片元的 HUD(那才烤进纹理);想在同一元素上「先 GL 再 2D」。

2. 哪层必须 CPU,哪层进 GPU

归属
世界对象、笔划点、相机CPU 文档
mesh / 纹理GPU(仅 GL canvas)
笔迹栅格2D backing store,仍是投影
工具条DOM,不要画进场景 FBO

命中:DOM 可点则 DOM;否则 2D 命中笔划(CPU 折线);都没有再 GL 拾取。拖轨道球时 overlay pointer-events: none,IME 框除外,见 Overlay

导出:分别画离屏再合成,或本帧 GL 读 FBO(左下原点,翻 Y)再 drawImage 到 2D。preserveDrawingBuffer 默认 false,读默认缓冲必须在同一帧提交前。GL 表面若被跨源纹理 taint,整条合成链 toBlob 失败。

禁止热路径:每帧 readPixels 全屏到 2D;每帧 texImage2D(inkCanvas) 只为「统一进 shader」。WebGPU 同样排他:getContext("webgpu") 后不能再要 2d

3. 失败形态

症状原因
getContext("2d") 为 null已经要过 webgl2 / webgpu
笔迹和模型错开两套相机或一边忘了 DPR
拖到按钮轨道球断overlay 未在拖时关掉命中
导出全透明跨帧读默认缓冲且未 preserve
导出 SecurityErrorGL 已 taint 还 drawImage 进 2D

权威资料

核对日期:2026-08-26