Skip to main content

Chapter 97: WebGL Fundamentals

WebGL 的核心问题是:浏览器中的一块 canvas 如何通过 JavaScript 控制 GPU,最终稳定画出一帧图像。读完本章,读者应能从一个 WebGL 黑屏、纹理错误、窗口 resize 后画面拉伸或帧率下降的现象,定位到 context、shader、program、attribute、uniform、texture、draw call、drawing buffer 和浏览器合成之间的具体环节。

本章以一个带纹理的 quad 为贯穿材料。这个 quad 只有四个顶点、一张贴图、一个 vertex shader、一个 fragment shader 和一次 drawElements,但它已经覆盖 WebGL 基础应用的关键路径:创建 canvas 上下文,编译 shader,链接 program,上传 buffer,绑定 attribute,设置 uniform,绑定 texture unit,提交 draw call,并在浏览器把 drawing buffer 合成到页面之前完成 viewport 和状态设置。

WebGL 的工作方式接近 OpenGL ES 的状态机。JavaScript 调用修改 WebGL context 中的状态,真正的 GPU 执行通常排队发生。浏览器还会在 API 调用边界做安全校验、资源格式检查和跨域限制。理解 WebGL 时,应把它同时看作图形 API、浏览器 Web API 和 GPU 提交入口;只从其中一个角度看,都会漏掉黑屏、卡顿和兼容性问题的来源。

本章的结论可以先压缩成一句话:一个可靠的 WebGL 基础渲染器,必须把一帧拆成“上下文能力 → shader program → 顶点输入 → uniform 与 texture → draw state → framebuffer 与浏览器事件”这条顺序,并在每个环节留下可检查的证据。

97.1 WebGL 的发展与基本设计理念

WebGL 是浏览器暴露给 JavaScript 的 GPU 图形接口。它把 GPU 加速的 2D / 3D 渲染接入 HTML canvas,让网页能在页面内部绘制交互式图形。MDN 对 WebGL 的定位是面向浏览器的高性能 2D / 3D 图形 API,WebGL 1.0 接近 OpenGL ES 2.0,WebGL 2 则引入 OpenGL ES 3.0 的大量能力,具体接口入口可以从 MDN WebGL APIKhronos WebGL 1.0 Specification 回溯。

WebGL 在 Web 平台中的存在意义,是把页面级 UI 和 GPU 级图形放进同一个分发模型。传统原生图形程序依赖操作系统窗口、驱动、原生 API 和本地安装包;WebGL 程序依赖浏览器、JavaScript、canvas、GPU 后端和页面生命周期。这个差异直接改变了工程判断:WebGL 应用要同时处理 GPU 状态、浏览器事件、资源加载、跨域策略、页面 resize、设备像素比、tab 切换和 context loss。

WebGL 的基本设计有三层对象。第一层是 DOM 中的 canvas,它提供一块可显示区域,并通过 getContext("webgl")getContext("webgl2") 创建渲染上下文。第二层是 WebGLRenderingContextWebGL2RenderingContext,它承载状态机、资源句柄、错误状态和绘制命令。第三层是 GPU 后端,浏览器会把 WebGL 命令翻译成平台图形接口,例如 OpenGL、Direct3D、Metal 或 Vulkan 后端;这个翻译层由浏览器实现决定,应用只能通过 WebGL 规范和能力查询观察它。

带纹理 quad 的第一条路径由 context 创建开始:

const canvas = document.querySelector("canvas");
const gl = canvas.getContext("webgl", {
alpha: true,
antialias: true,
powerPreference: "default",
});

if (!gl) {
throw new Error("WebGL context creation failed");
}

这段代码得到的并非一个“GPU 对象全集”,而是一组经过浏览器约束的 API 入口。powerPreference 只是浏览器选择 GPU 配置时的提示;实际选择由用户代理、电源状态、设备策略和驱动可用性共同决定。alphaantialiasdepthstencil 这类上下文属性会影响 drawing buffer 的格式、内存占用和合成成本。工程上应把 context attributes 看作渲染目标配置的一部分,和 framebuffer 格式、后处理链、页面透明合成一起评估。

WebGL 继承了 OpenGL ES 2.0 的可编程管线思路。固定光照、固定矩阵堆栈和传统立即模式调用没有出现在 WebGL 基础模型中;应用需要自己提供 shader,并把顶点、矩阵、贴图和材质参数显式绑定到 program。对于一个 quad,顶点位置由 buffer 提供,MVP 或屏幕空间变换由 uniform 提供,贴图由 texture object 与 texture unit 提供,最终 fragment shader 输出颜色。

WebGL 的第二个设计重点是安全和可移植性。网页代码来自网络,浏览器需要阻止 shader、纹理上传、buffer 访问和读回路径越过安全边界。结果是 WebGL 会执行大量参数校验,并通过 getError、console warning、context loss 和资源初始化规则把错误暴露出来。应用看到的性能上限也会受浏览器主线程、GPU 进程、驱动沙箱、页面可见性和系统资源压力影响。

WebGL 的第三个设计重点是能力分层。WebGL 1 覆盖基础可编程图形管线;WebGL 2 加入更多纹理格式、uniform buffer、transform feedback、query、vertex array object 等能力;扩展机制再补充平台相关能力。基础渲染器的稳定做法是先写 WebGL 1 可运行的核心路径,再用 gl instanceof WebGL2RenderingContextgl.getExtension(...)gl.getParameter(...) 打开增强路径。这样可以把“功能是否存在”和“功能如何使用”分开判断。

97.2 WebGL 渲染管线的核心阶段

WebGL 渲染管线可以按一帧中的数据流理解。JavaScript 先准备状态和资源,然后一次 draw call 把顶点输入送入 vertex shader,经过图元装配、裁剪、光栅化后进入 fragment shader,最后经过深度、模板、混合和颜色写入到 drawing buffer。浏览器在帧边界把 drawing buffer 和页面其他内容合成,用户看到最终画面。

下面的图只覆盖单个带纹理 quad 的一次 draw call,不包含多 pass、离屏 framebuffer、后处理和 WebXR 提交流程:

JavaScript 设置状态阶段解决“这一帧用哪些资源和规则绘制”。对于 quad,应用要选择 program,绑定顶点 buffer 和 index buffer,设置 viewport,开启或关闭 depth test、blend、cull face,绑定 texture unit,并把 sampler uniform 指向对应 unit。WebGL 是状态机,draw call 使用的是提交瞬间 context 中的当前状态;黑屏排查时,应从 draw call 前的当前状态回溯资源来源。

顶点输入阶段解决“每个 vertex shader invocation 从哪里读数据”。gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer) 只把 buffer 设为当前数组缓冲;gl.vertexAttribPointer(...) 才把当前 ARRAY_BUFFER、偏移、步长、类型和 attribute location 关联起来。gl.enableVertexAttribArray(location) 再允许 vertex shader 按顶点读取该 attribute。若 location 错、buffer 绑定错、stride / offset 错或 attribute 未启用,vertex shader 会收到错误输入,结果可能是空画面、几何塌缩或 UV 错乱。

Vertex shader 阶段解决“顶点进入裁剪空间的位置”。WebGL 1 的 vertex shader 通常接收 attributeuniform,输出 gl_Position 和传给 fragment shader 的 varying。对于屏幕空间 quad,aPosition 可以直接写入 gl_Position;对于 3D 模型,应用会传入 model、view、projection 矩阵,把模型空间顶点变到 clip space。判断 vertex shader 是否正确,核心证据是 gl_Position 是否落在裁剪体内,以及传出的 varyings 是否和后续 fragment shader 匹配。

图元装配、裁剪和光栅化阶段解决“顶点如何变成片元”。drawArraysdrawElements 的 mode 决定点、线、三角形的装配方式;index buffer 决定顶点复用顺序;viewport 决定 NDC 到 drawing buffer 像素的映射。浏览器窗口 resize 后,若只改 CSS 尺寸而没有同步 canvas.widthcanvas.heightgl.viewport,同一组 clip space 坐标会映射到旧 drawing buffer,画面就会模糊、拉伸或只覆盖局部区域。

Fragment shader 阶段解决“每个片元输出什么颜色”。对于纹理 quad,fragment shader 用插值后的 UV 采样 sampler2D,再输出 gl_FragColor。这一步的输入来自三处:rasterizer 插值出的 varying,当前 program 中的 uniform 值,以及 sampler uniform 指向的 texture unit。纹理黑、花屏或全透明时,应同时检查 UV、texture object、texture parameters、mipmap 完整性、跨域加载和 sampler uniform。

测试、混合和颜色写入阶段解决“fragment 输出如何进入 render target”。Depth test 会按深度值决定片元保留;blend 会把 source color 和 destination color 合成;color mask 和 scissor 会限制写入范围。基础 2D quad 常用透明混合:

gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.disable(gl.DEPTH_TEST);

这组状态适合半透明 UI、粒子和贴图精灵。若同一应用渲染 3D 物体和透明 UI,应把不透明物体、透明物体、UI 分成清晰 draw 顺序,并在每个批次设置对应 depth 和 blend 状态。WebGL 不会按材质语义自动切换状态,状态错误会直接表现为遮挡错误、透明顺序错误或颜色累积异常。

Drawing buffer 和浏览器合成阶段解决“WebGL 输出如何进入页面”。WebGL 绘制目标默认是 context 关联的 drawing buffer,浏览器随后把它与 DOM、CSS、视频、Canvas 2D 等内容合成。CSS 尺寸、设备像素比和 drawing buffer 尺寸分离,是 WebGL 应用中常见的视觉边界。高 DPI 屏幕上,canvas.clientWidth 代表 CSS 像素,canvas.width 代表实际 drawing buffer 像素;应用要在 resize 时根据设备像素比更新后者,再同步 viewport

97.3 WebGL Shader Compile Link Uniform and Attribute Path

Shader path 负责把字符串形式的 GLSL ES 源码变成可用于 draw call 的 program。WebGL 中,compileShader 编译单个 vertex shader 或 fragment shader;linkProgram 把两类 shader 组合成一个 program,并建立 attribute、varying、uniform 和输出之间的匹配关系。MDN 的 compileShader() 文档说明它把 GLSL shader 编译为可被 WebGLProgram 使用的二进制数据,linkProgram() 文档说明链接阶段完成 vertex 与 fragment shader 的 GPU 程序准备。

带纹理 quad 的 shader 可以压缩到两段。Vertex shader 读取位置和 UV,把位置写到 clip space,把 UV 传给 fragment shader:

attribute vec2 aPosition;
attribute vec2 aUv;

varying vec2 vUv;

void main() {
vUv = aUv;
gl_Position = vec4(aPosition, 0.0, 1.0);
}

Fragment shader 接收插值后的 UV,并从 uTexture 指向的纹理采样:

precision mediump float;

uniform sampler2D uTexture;
varying vec2 vUv;

void main() {
gl_FragColor = texture2D(uTexture, vUv);
}

这两段 shader 隐含了三个约束。第一,aPositionaUv 必须在 program 链接后得到有效 location,并在 draw 前绑定到 buffer layout。第二,vUv 的类型和名称要在 vertex 与 fragment 两侧匹配,链接阶段会检查这个接口。第三,uTexture 是 sampler uniform,它的值是 texture unit 编号,例如 0 对应 gl.TEXTURE0,它并非 texture object 句柄。

基础编译和链接路径应把错误信息带回源码阶段:

function compileShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);

if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const log = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
throw new Error(`Shader compile failed:\n${log}`);
}

return shader;
}

function createProgram(gl, vertexSource, fragmentSource) {
const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexSource);
const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
const program = gl.createProgram();

gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);

gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);

if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const log = gl.getProgramInfoLog(program);
gl.deleteProgram(program);
throw new Error(`Program link failed:\n${log}`);
}

return program;
}

这段代码只在初始化或资源重建时使用同步状态查询。MDN 的 WebGL best practices 提醒,getShader/ProgramParameter、info log、getParameterreadPixels 等查询可能触发同步等待。工程上应把 shader 编译、链接和反射集中到加载阶段,并把每帧路径保持为少量状态绑定和 draw call。

Attribute path 从 JavaScript buffer 进入 vertex shader。以下代码为 interleaved buffer 指定 aPositionaUv 的布局,每个顶点包含 x, y, u, v 四个 float:

const vertices = new Float32Array([
-1, -1, 0, 0,
1, -1, 1, 0,
1, 1, 1, 1,
-1, 1, 0, 1,
]);
const indices = new Uint16Array([0, 1, 2, 0, 2, 3]);

const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

const stride = 4 * Float32Array.BYTES_PER_ELEMENT;
const positionLocation = gl.getAttribLocation(program, "aPosition");
const uvLocation = gl.getAttribLocation(program, "aUv");

gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, stride, 0);

gl.enableVertexAttribArray(uvLocation);
gl.vertexAttribPointer(
uvLocation,
2,
gl.FLOAT,
false,
stride,
2 * Float32Array.BYTES_PER_ELEMENT,
);

vertexAttribPointer 的关键语义是“把当前绑定的 ARRAY_BUFFER 按给定 layout 连接到 attribute location”。MDN 对 vertexAttribPointer() 的说明也落在这个点:它把当前绑定到 ARRAY_BUFFER 的 buffer 连接到通用顶点 attribute,并指定 layout。排查 attribute 问题时,先看 program 当前生效,再看 location,再看 buffer 是否仍然绑定,再看 stride / offset 是否以字节计算。

Uniform path 从 JavaScript 的常量或小数组进入 program。Uniform location 属于某个已链接 program;program 重新链接后,旧 location 和旧 uniform 值都应视为失效。对于纹理 quad,uTexture 应绑定为整数 texture unit:

const textureLocation = gl.getUniformLocation(program, "uTexture");

gl.useProgram(program);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform1i(textureLocation, 0);

Texture path 要同时满足 object、unit、parameter 和 image data 四个条件。gl.createTexture() 只创建句柄;gl.bindTexture 把它绑定到当前 texture target;gl.texImage2D 上传像素;gl.texParameteri 决定采样和 wrap 规则;gl.activeTexture 决定后续 bind 作用于哪个 texture unit;sampler uniform 决定 shader 访问哪个 unit。若图片来自跨域来源,还需要图片响应头和加载属性满足浏览器安全策略,否则上传路径会失败。

Draw call path 把前面所有状态合并成一次提交:

gl.useProgram(program);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform1i(textureLocation, 0);

gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);

这段调用的可复查顺序是固定的:program 是否链接成功,attribute 是否找到并启用,buffer layout 是否匹配 shader,texture 是否完整,uniform 是否写入当前 program,draw mode、count、type、offset 是否匹配 index buffer。黑屏通常来自其中一个环节的断裂,而非 GPU “随机失败”。

97.4 实际约束与性能限制

WebGL 的实际约束来自浏览器、设备、驱动和规范共同形成的边界。一个基础渲染应用应把性能和兼容性判断落到五类证据:WebGL error、capability limit、extension availability、context event、frame time / main-thread stall。只看 FPS 无法区分 CPU 提交、shader 执行、纹理上传、浏览器合成和同步查询的成本来源。

第一类约束是能力限制。不同设备的 MAX_TEXTURE_SIZEMAX_TEXTURE_IMAGE_UNITSMAX_VERTEX_ATTRIBS、precision、renderbuffer size 和 extension 集合会不同。加载阶段应查询 limits,并把结果写入 renderer profile:

const profile = {
maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
maxTextureUnits: gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS),
maxVertexAttribs: gl.getParameter(gl.MAX_VERTEX_ATTRIBS),
supportsVao: Boolean(gl.getExtension("OES_vertex_array_object")),
};

这段代码回答的是“当前设备允许怎样的资源规模和输入布局”。它适合初始化阶段、调试面板和 fallback 决策;放进每帧路径会增加同步查询风险。WebGL renderer 应把 profile 固定下来,再让资源系统按 profile 选择纹理尺寸、材质变体、批处理策略和扩展路径。

第二类约束是错误和同步查询。getError 可以定位 API 错误,但频繁调用会扰动性能。实践中可在开发构建中用 wrapper 检查关键调用,在生产构建中只保留 context loss、资源加载错误、初始化失败和必要的 fallback 记录。MDN best practices 中明确指出 WebGL 错误会进入浏览器 console,过多错误还会削弱后续调试信息;稳定应用应让正常帧保持无 WebGL error。

第三类约束是 texture upload 和内存。texImage2DtexSubImage2D、视频纹理更新、mipmap 生成和 render target resize 都可能触发数据传输、格式转换或 pipeline flush。网页还缺少可移植的直接 VRAM 容量查询接口,因此 WebGL 应用常用像素预算、纹理缓存上限和分辨率缩放来控制内存。对移动设备和高 DPI 屏幕,drawing buffer 面积会成倍放大颜色、深度、模板、后处理贴图和多重采样成本。

第四类约束是浏览器主线程。WebGL API 调用通常从 JavaScript 发起,和布局、事件、动画回调、资源解码共用主线程资源。若每帧创建 buffer、重新编译 shader、频繁 getParameter、同步读回像素或上传大纹理,页面会出现输入延迟和滚动卡顿。稳定路径应把资源创建移动到加载阶段,把每帧变动压缩成 uniform 更新、少量 buffer subdata、纹理切换和批量 draw。

第五类约束是 context loss。WebGL context 可能因为电源事件、GPU reset、系统内存压力或浏览器策略而丢失。Khronos 规范把 context loss 作为 WebGL 生命周期的一部分,并说明恢复后旧的 texture、buffer、program 和 extension 都需要重建。WEBGL_lose_context 扩展还能在测试中模拟丢失与恢复,MDN 对 WEBGL_lose_context 的说明也把它作为触发 webglcontextlostwebglcontextrestored 事件的测试入口。

基础应用应把 context loss 当作资源系统重建问题处理:

canvas.addEventListener("webglcontextlost", (event) => {
event.preventDefault();
stopRenderLoop();
});

canvas.addEventListener("webglcontextrestored", () => {
rebuildGpuResources();
startRenderLoop();
});

这段代码的要点在于恢复后重新创建 GPU 资源。JavaScript 中保存的图片、几何数据、shader source 和材质描述属于 CPU 侧材料;WebGLTexture、WebGLBuffer、WebGLProgram 等句柄属于 context 内资源。context 丢失时,CPU 侧材料可以继续作为重建依据,旧 GPU 句柄则应全部替换。

WebGL 性能判断顺序应从大到小。先确认 canvas 实际 drawing buffer 尺寸是否合理,再确认 draw call 数量和状态切换规模,再确认纹理上传是否出现在帧内,再确认 shader 是否把大量计算放在 fragment 阶段,再确认同步查询和 CPU 读回是否阻塞主线程。这个顺序能把大多数基础应用的瓶颈定位到可修改对象,而非停留在“浏览器慢”这类不可操作判断。

97.5 用 WebGL 构建基础渲染应用

一个基础 WebGL 应用可以拆成七个可维护模块:context 创建、shader program、geometry buffer、texture resource、resize、render loop、context loss restore。这个拆分的价值在于每个模块都对应一组可观察证据。黑屏时看 program log 和 draw state;贴图错误时看 texture path;窗口变化时看 resize path;恢复失败时看资源重建路径。

下面的骨架保留关键路径,省略图片加载和矩阵库细节。它的目标是说明模块边界和状态顺序:

function createRenderer(canvas) {
let gl = canvas.getContext("webgl");
let resources = null;
let animationFrame = 0;

if (!gl) {
throw new Error("WebGL context creation failed");
}

function resize() {
const width = Math.max(1, Math.floor(canvas.clientWidth * window.devicePixelRatio));
const height = Math.max(1, Math.floor(canvas.clientHeight * window.devicePixelRatio));

if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
}

gl.viewport(0, 0, canvas.width, canvas.height);
}

function rebuild() {
resources = {
program: createProgram(gl, vertexSource, fragmentSource),
geometry: createQuadGeometry(gl),
texture: createPlaceholderTexture(gl),
};
}

function draw() {
resize();

gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);

gl.useProgram(resources.program);
bindQuadGeometry(gl, resources.program, resources.geometry);
bindTextureUniform(gl, resources.program, resources.texture, "uTexture", 0);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);

animationFrame = requestAnimationFrame(draw);
}

function start() {
rebuild();
animationFrame = requestAnimationFrame(draw);
}

function stop() {
cancelAnimationFrame(animationFrame);
}

canvas.addEventListener("webglcontextlost", (event) => {
event.preventDefault();
stop();
resources = null;
});

canvas.addEventListener("webglcontextrestored", () => {
gl = canvas.getContext("webgl");
start();
});

return { start, stop };
}

resize 模块负责把 CSS 尺寸变成 drawing buffer 尺寸,并同步 viewport。这是所有 WebGL 基础应用都应固定下来的入口,因为它连接页面布局和 GPU 像素坐标。若应用提供动态分辨率缩放,缩放因子也应放在这个模块内统一计算,再把实际 back buffer 尺寸暴露给后处理、UI 和拾取逻辑。

rebuild 模块负责创建 context 相关资源。Program、buffer、texture、framebuffer、renderbuffer 和扩展对象都属于这个范围。它们可以由 CPU 侧材料重建:shader source、typed array、图片位图、材质参数和 renderer profile。这个边界让 context loss 处理变得清晰,也让资源热更新、调试重载和设备 fallback 共用同一套流程。

draw 模块负责每帧最小状态集。对于 textured quad,它需要清屏、使用 program、绑定 geometry、绑定 texture uniform、提交 draw call。真实项目会有多个 pass 和多个材质,但基础判断不变:每个 draw call 前都要能回答“当前 program 是谁,顶点输入来自哪里,uniform 和 texture 是否已经写入,framebuffer 与 viewport 是否匹配,状态是否服务当前 pass”。

bindQuadGeometry 这类函数应把 attribute location 和 buffer layout 绑定写得可复查。一个常见写法是在 program 链接后缓存 locations:

function getProgramLocations(gl, program) {
return {
aPosition: gl.getAttribLocation(program, "aPosition"),
aUv: gl.getAttribLocation(program, "aUv"),
uTexture: gl.getUniformLocation(program, "uTexture"),
};
}

缓存 location 的意义是把“shader 接口反射”从每帧路径移到初始化阶段。若链接后的 program 没有使用某个 uniform,getUniformLocation 可能返回 null;若 attribute 被优化掉或名字写错,getAttribLocation 可能返回 -1。初始化阶段应处理这些结果,并给出面向 shader 接口的错误信息。

createPlaceholderTexture 这类函数应先创建可采样的占位纹理,再在图片加载完成后替换内容。这样 draw loop 可以稳定运行,资源异步加载只改变纹理内容:

function createPlaceholderTexture(gl) {
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);

gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
1,
1,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
new Uint8Array([255, 255, 255, 255]),
);

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

return texture;
}

这段代码选择 CLAMP_TO_EDGELINEAR,可以让 1x1 占位纹理立即成为完整可采样纹理。实际图片若使用非 2 的幂尺寸,在 WebGL 1 中也应使用兼容的 wrap 和 mipmap 策略;若要启用 mipmap,应保证尺寸、过滤模式和 generateMipmap 调用满足 WebGL 1 规则,或转向 WebGL 2 的更宽松能力。

基础渲染应用的调试顺序可以固定为八步。先确认 context 成功创建;再确认 shader compile log 和 program link log;再确认 attribute location、buffer layout、index type;再确认 uniform location 和 uniform 写入 program;再确认 texture unit、texture parameters、图片来源和 mipmap 完整性;再确认 viewport、drawing buffer 尺寸和 clear color;再确认 WebGL error 和浏览器 console;最后确认 context loss、resize 和资源重建路径。这个顺序覆盖了本章 quad 的全部数据路径,也能迁移到 sprite、粒子、简单 3D mesh 和后处理 full-screen pass。

最小自检任务

有一个 WebGL 1 页面要绘制一张全屏贴图 quad。页面没有 shader compile error,也没有 program link error。drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0) 每帧都执行,浏览器 console 没有持续 WebGL error。实际画面是全黑;resize 窗口后,有时只在左下角出现一小块贴图。请按本章的检查顺序写出排查路径,并指出最可能的三类问题。

答案要点

先检查 context 和 drawing buffer。全屏贴图只覆盖左下角,优先看 canvas.widthcanvas.height 是否跟随 CSS 尺寸和 devicePixelRatio 更新,再看 gl.viewport(0, 0, canvas.width, canvas.height) 是否在 resize 后执行。viewport 仍保留旧尺寸时,clip space 到像素的映射会落在旧 drawing buffer 区域,出现局部绘制或拉伸。

再检查 attribute path。Program 链接成功只说明 shader 接口可以组合,仍需要确认 aPositionaUv 的 location 有效,enableVertexAttribArray 已执行,vertexAttribPointer 的 stride 和 offset 按字节计算,且调用时正确的 ARRAY_BUFFER 处于绑定状态。若 aPosition 读错,quad 可能被裁剪;若 aUv 读错,贴图可能采样到黑色边缘或固定像素。

然后检查 texture 与 sampler path。确认 gl.activeTexture(gl.TEXTURE0)gl.bindTexture(gl.TEXTURE_2D, texture)gl.uniform1i(uTexture, 0) 指向同一个 texture unit;确认 texture parameters 让当前图片成为完整纹理;确认图片跨域和加载时序已经满足上传条件。Sampler uniform 没有写入当前 program,或者 texture 仍是未上传状态时,fragment shader 的采样结果就可能稳定为黑色。

最后检查 draw state。全屏贴图通常关闭 depth test,使用清晰的 blend 状态,并在 draw 前设置正确 framebuffer。若 depth buffer 中已有更近的值,或 color mask / scissor 限制写入,fragment shader 即使输出正确颜色,也可能无法进入 drawing buffer。完整结论应把 resize、attribute、texture / sampler 和 draw state 四个环节逐个排除,并把黑屏归因到已经命中的证据点。

本章知识点总结

  • WebGL 定位:WebGL 把浏览器 canvas、JavaScript 状态机和 GPU 图形管线连接成网页内的可编程渲染路径。
  • Context 边界WebGLRenderingContext 是经过浏览器安全、能力和生命周期约束的 API 入口,实际 GPU 后端由浏览器实现决定。
  • 基础路径:可靠渲染应按 context、program、attribute、uniform、texture、draw call、drawing buffer 的顺序建立证据链。
  • 管线阶段:WebGL 一次 draw call 会经过 JavaScript 状态设置、顶点输入、vertex shader、光栅化、fragment shader、测试混合和浏览器合成。
  • Attribute 语义vertexAttribPointer 绑定的是当前 ARRAY_BUFFER 与 attribute layout,stride 和 offset 必须按字节解释。
  • Uniform 语义:Uniform location 属于已链接 program,sampler uniform 的值是 texture unit 编号。
  • Texture 路径:Texture object、texture unit、sampler uniform、参数、像素上传和跨域策略共同决定 shader 能否正确采样。
  • Resize 规则:CSS 尺寸、drawing buffer 尺寸和 viewport 需要同步,否则 clip space 到像素的映射会产生模糊、拉伸或局部绘制。
  • 性能证据:WebGL 性能判断应查看 drawing buffer 面积、draw call 数、纹理上传、shader 执行、同步查询和主线程压力。
  • 能力分层:WebGL 1、WebGL 2 和扩展应通过 profile 查询分层启用,资源系统按 profile 选择 fallback。
  • Context loss:Context 恢复后旧 GPU 句柄失效,应用应保留 CPU 侧材料并重建 program、buffer、texture 和扩展状态。
  • 调试顺序:黑屏排查应从 context 和 shader log 开始,沿 attribute、uniform、texture、viewport、draw state 和 WebGL error 逐步定位。