从 WebGL 到 WebGPU

原文标题:WebGPU from WebGL
原作者:webgpufundamentals Contributors
原始链接:https://webgpufundamentals.org/webgpu/lessons/webgpu-from-webgl.html

本文面向已经熟悉 WebGL、希望开始使用 WebGPU 的开发者。

如果你从 WebGL 转向 WebGPU,会发现许多概念是相通的。两者都允许你在 GPU 上运行小型函数。WebGL 有顶点着色器(vertex shader)和片元着色器(fragment shader);WebGPU 除此之外还有计算着色器(compute shader)。WebGL 使用 GLSL 作为着色语言,而 WebGPU 使用 WGSL。虽然语言不同,但核心概念大体一致。

两种 API 都有属性(attribute)——从缓冲区(buffer)中取出数据并喂给顶点着色器的每次迭代;都有统一变量(uniform)——被着色器函数的所有迭代共享的值;都有可变变量(varying)——从顶点着色器传递到片元着色器,并在光栅化时进行插值;都有纹理(texture)和采样器(sampler)——提供 2D/3D 数据并采样(将多个像素过滤为一个值);都能渲染到纹理;并且都有大量关于像素混合(blending)、深度缓冲(depth buffer)、模板缓冲(stencil buffer)的设置。

最大的区别在于:WebGL 是有状态 API,而 WebGPU 不是。在 WebGL 中,存在大量全局状态:当前绑定的纹理、缓冲区、程序,以及混合、深度、模板设置等。通过调用 gl.bindBuffer、gl.enable、gl.blendFunc 等 API 来设置,并且它们会全局保持,直到被改变。

相反,WebGPU 几乎没有全局状态。取而代之的是**渲染管线(render pipeline)和渲染通道(render pass)**的概念,它们一起包含了 WebGL 中原本全局的大部分状态。纹理、属性、缓冲区以及各种其他设置都包含在其中。未显式设置的选项使用默认值。管线不可修改——创建后即为不可变(immutable)对象,若需要不同设置则必须创建新的管线。渲染通道也有状态,但仅限于该通道内部。

第二大区别是 WebGPU 更底层。WebGL 中许多事物是通过名字连接的。例如,在 GLSL 中声明一个 uniform,然后查询它的位置:

loc = gl.getUniformLocation(program, 'nameOfUniform');

另一个例子是 varying:在顶点着色器中使用 varying vec2 v_texcoord 或 out vec2 v_texcoord,在片元着色器中声明对应的 varying 并命名为 v_texcoord。好处是拼写错误会报错。

而 WebGPU 完全通过索引或字节偏移来连接。你不会像 WebGL 那样创建单独的 uniform,而是声明 uniform 块(uniform block,即声明 uniform 的结构体)。然后由你负责手动组织传递给着色器的数据以匹配该结构。注意:WebGL2 也有类似概念(Uniform Blocks),但 WebGL2 仍然支持按名字访问 uniform;且即便需要字节偏移,WebGL2 也能查询偏移量和块位置。WebGPU 则一切靠字节偏移或索引(常称为“位置 location”),且没有查询 API,完全靠手动保持同步。

用 JavaScript 类比:

function likeWebGL(inputs) {
  const {position, texcoords, normal, color} = inputs;
  ...
}

function likeWebGPU(inputs) {
  const [position, texcoords, normal, color] = inputs;
  ...
}

likeWebGL 按名字连接,参数顺序无关,也能跳过缺失参数;likeWebGPU 则按数组索引传递,必须知道每个参数的索引位置。在 WebGPU 中,保持 WGSL 内部与 JavaScript/WASM 外部的位置同步完全由你负责。

其他显著差异

  • 画布(Canvas)
    WebGL 为你管理画布,创建上下文时选择抗锯齿、preserveDrawingBuffer、模板、深度、alpha,之后只需设置 canvas.width 和 canvas.height。
    WebGPU 需要自己完成大部分工作:深度缓冲需自己创建(可带模板缓冲),抗锯齿需自己创建多重采样纹理并解析到画布纹理。但因此一个 WebGPU 设备可以渲染到多个画布。

  • WebGPU 不生成 mipmap
    WebGL 中可以调用 gl.generateMipmap 自动生成所有 mip 层级;WebGPU 没有此函数,需要自行生成(参考本文)。

  • WebGPU 强制要求采样器(sampler)
    WebGL1 中没有采样器概念(由内部处理),WebGL2 可选,WebGPU 必需。

  • 缓冲区和纹理不可调整大小
    WebGL 可在运行时重新分配缓冲区或纹理;WebGPU 中纹理和缓冲区的大小、用途、格式不可变,只能修改内容。需要更新尺寸时必须销毁旧资源并创建新资源。

着色器对比

下面是一个绘制带纹理、受光照三角形的着色器,GLSL 与 WGSL 对照:

// GLSL
const vSrc = `
uniform mat4 u_worldViewProjection;
uniform mat4 u_worldInverseTranspose;

attribute vec4 a_position;
attribute vec3 a_normal;
attribute vec2 a_texcoord;

varying vec2 v_texCoord;
varying vec3 v_normal;

void main() {
  gl_Position = u_worldViewProjection * a_position;
  v_texCoord = a_texcoord;
  v_normal = (u_worldInverseTranspose * vec4(a_normal, 0)).xyz;
}
`;

const fSrc = `
precision highp float;

varying vec2 v_texCoord;
varying vec3 v_normal;

uniform sampler2D u_diffuse;
uniform vec3 u_lightDirection;

void main() {
  vec4 diffuseColor = texture2D(u_diffuse, v_texCoord);
  vec3 a_normal = normalize(v_normal);
  float l = dot(a_normal, u_lightDirection) * 0.5 + 0.5;
  gl_FragColor = vec4(diffuseColor.rgb * l, diffuseColor.a);
}
`;
// WGSL
const shaderSrc = `
struct VSUniforms {
  worldViewProjection: mat4x4f,
  worldInverseTranspose: mat4x4f,
};
@group(0) binding(0) `var<uniform>` vsUniforms: VSUniforms;

struct MyVSInput {
    @location(0) position: vec4f,
    @location(1) normal: vec3f,
    @location(2) texcoord: vec2f,
};

struct MyVSOutput {
  @builtin(position) position: vec4f,
  @location(0) normal: vec3f,
  @location(1) texcoord: vec2f,
};

@vertex
fn myVSMain(v: MyVSInput) -> MyVSOutput {
  var vsOut: MyVSOutput;
  vsOut.position = vsUniforms.worldViewProjection * v.position;
  vsOut.normal = (vsUniforms.worldInverseTranspose * vec4f(v.normal, 0.0)).xyz;
  vsOut.texcoord = v.texcoord;
  return vsOut;
}

struct FSUniforms {
  lightDirection: vec3f,
};

@group(0) binding(1) `var<uniform>` fsUniforms: FSUniforms;
@group(0) binding(2) var diffuseSampler: sampler;
@group(0) binding(3) var diffuseTexture: `texture_2d<f32>`;

@fragment
fn myFSMain(v: MyVSOutput) -> @location(0) vec4f {
  var diffuseColor = textureSample(diffuseTexture, diffuseSampler, v.texcoord);
  var a_normal = normalize(v.normal);
  var l = dot(a_normal, fsUniforms.lightDirection) * 0.5 + 0.5;
  return vec4f(diffuseColor.rgb * l, diffuseColor.a);
}
`;

两者核心相似,vec4 变成 vec4f,mat4 变成 mat4x4f,其他如 int→i32,uint→u32,ivec2→vec2i,uvec3→vec3u。GLSL 风格类似 C/C++,WGSL 类似 Rust:类型在 GLSL 中位于左侧,在 WGSL 中位于右侧。WGSL 支持类型推断,而 GLSL 必须显式声明类型。

最大的区别是 @??? 注解,它们显式声明数据来源。uniform 的 @group(?) @binding(?) 必须保证不冲突。属性通过 @location 指定,必须手动给出。varying 在 WGSL 中通过结构体定义,并指定每个字段的位置。片元着色器的输入结构无需与顶点输出结构同名,只要位置匹配即可。gl_Position 在 WGSL 中对应 @builtin(position),片元输出用 @location(0) 标识。

获取 API

// WebGL
function main() {
  const gl = document.querySelector('canvas').getContext('webgl');
  if (!gl) {
    fail('need webgl');
    return;
  }
}
main();
// WebGPU
async function main() {
  const adapter = await navigator.gpu?.requestAdapter();
  const device = await adapter?.requestDevice();
  if (!device) {
    fail('need a browser that supports WebGPU');
    return;
  }
}
main();

adapter 代表 GPU 本身,device 代表该 GPU 上的 API 实例。最大区别是 WebGPU 获取 API 是异步的。

创建缓冲区

// WebGL
function createBuffer(gl, data, type = gl.ARRAY_BUFFER) {
  const buf = gl.createBuffer();
  gl.bindBuffer(type, buf);
  gl.bufferData(type, data, gl.STATIC_DRAW);
  return buf;
}
const positionBuffer = createBuffer(gl, positions);
const normalBuffer   = createBuffer(gl, normals);
const texcoordBuffer = createBuffer(gl, texcoords);
const indicesBuffer  = createBuffer(gl, indices, gl.ELEMENT_ARRAY_BUFFER);
// WebGPU
function createBuffer(device, data, usage) {
  const buffer = device.createBuffer({
    size: data.byteLength,
    usage,
    mappedAtCreation: true,
  });
  const dst = new data.constructor(buffer.getMappedRange());
  dst.set(data);
  buffer.unmap();
  return buffer;
}
const positionBuffer = createBuffer(device, positions, GPUBufferUsage.VERTEX);
const normalBuffer   = createBuffer(device, normals, GPUBufferUsage.VERTEX);
const texcoordBuffer = createBuffer(device, texcoords, GPUBufferUsage.VERTEX);
const indicesBuffer  = createBuffer(device, indices, GPUBufferUsage.INDEX);

两者差异不大,主要是调用函数不同。

创建纹理与采样器

// WebGL
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 2, 2, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([...]));
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
// WebGPU
const tex = device.createTexture({
  size: [2, 2],
  format: 'rgba8unorm',
  usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
});
device.queue.writeTexture(
  { texture: tex },
  new Uint8Array([...]),
  { bytesPerRow: 8, rowsPerImage: 2 },
  { width: 2, height: 2 },
);
const sampler = device.createSampler({
  magFilter: 'nearest',
  minFilter: 'nearest',
});

WebGPU 需要设置纹理用途标志,并且必须显式创建采样器。

编译着色器

// WebGL
const vs = createShader(gl, gl.VERTEX_SHADER, vSrc);
const fs = createShader(gl, gl.FRAGMENT_SHADER, fSrc);
// WebGPU
const shaderModule = device.createShaderModule({ code: shaderSrc });

WebGL 需要手动检查编译状态并获取错误日志;WebGPU 大多数实现会将错误打印到 JavaScript 控制台,更加方便。

链接程序 / 设置管线

WebGL 中着色器链接、顶点属性设置、图元类型、深度缓冲使用等分散在不同调用中;WebGPU 将这些合并到渲染管线的创建过程。

// WebGL 简化
const program = createProgram(gl, vs, fs);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(positionLoc);
// ... 其他属性
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
// WebGPU
const pipeline = device.createRenderPipeline({
  layout: 'auto',
  vertex: {
    module: shaderModule,
    buffers: [
      { arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] },
      { arrayStride: 12, attributes: [{ shaderLocation: 1, offset: 0, format: 'float32x3' }] },
      { arrayStride: 8,  attributes: [{ shaderLocation: 2, offset: 0, format: 'float32x2' }] },
    ],
  },
  fragment: {
    module: shaderModule,
    targets: [{ format: presentationFormat }],
  },
  primitive: { topology: 'triangle-list', cullMode: 'back' },
  depthStencil: { depthWriteEnabled: true, depthCompare: 'less', format: 'depth24plus' },
  ...(canvasInfo.sampleCount > 1 && { multisample: { count: canvasInfo.sampleCount } }),
});

注意 createRenderPipeline 是一个较慢的调用,内部可能会调整着色器。顶点缓冲区布局在此声明,实际缓冲区在绘制时绑定。若要绘制不同图元(三角形或线)或不同顶点布局,需要创建多个管线。

准备 uniform

// WebGL
const u_lightDirectionLoc = gl.getUniformLocation(program, 'u_lightDirection');
const u_diffuseLoc = gl.getUniformLocation(program, 'u_diffuse');
const u_worldInverseTransposeLoc = gl.getUniformLocation(program, 'u_worldInverseTranspose');
const u_worldViewProjectionLoc = gl.getUniformLocation(program, 'u_worldViewProjection');
// WebGPU
const vUniformBufferSize = 2 * 16 * 4; // 2 mat4s * 16 floats * 4 bytes
const fUniformBufferSize = 3 * 4;      // 1 vec3 * 3 floats * 4 bytes

const vsUniformBuffer = device.createBuffer({
  size: vUniformBufferSize,
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const fsUniformBuffer = device.createBuffer({
  size: fUniformBufferSize,
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const vsUniformValues = new Float32Array(2 * 16);
const worldViewProjection = vsUniformValues.subarray(0, 16);
const worldInverseTranspose = vsUniformValues.subarray(16, 32);
const fsUniformValues = new Float32Array(3);
const lightDirection = fsUniformValues.subarray(0, 3);

WebGPU 没有查询 uniform 位置的 API,必须手动计算字节偏移和大小。

准备绘制:绑定组与渲染通道描述符

// WebGPU
const bindGroup = device.createBindGroup({
  layout: pipeline.getBindGroupLayout(0),
  entries: [
    { binding: 0, resource: vsUniformBuffer },
    { binding: 1, resource: fsUniformBuffer },
    { binding: 2, resource: sampler },
    { binding: 3, resource: tex },
  ],
});

绑定组中的 binding 必须与着色器中 @binding 匹配。

渲染通道描述符:

// WebGPU
const renderPassDescriptor = {
  colorAttachments: [{
    clearValue: [0.5, 0.5, 0.5, 1],
    loadOp: 'clear',
    storeOp: 'store',
  }],
  depthStencilAttachment: {
    depthClearValue: 1,
    depthLoadOp: 'clear',
    depthStoreOp: 'store',
  },
};

WebGL 中渲染到画布时这些设置由 API 自动处理,WebGPU 中即使渲染到画布也需要自行描述。

设置 uniform 数据

// WebGL
gl.uniform3fv(u_lightDirectionLoc, v3.normalize([1, 8, -10]));
gl.uniform1i(u_diffuseLoc, 0);
gl.uniformMatrix4fv(u_worldInverseTransposeLoc, false, m4.transpose(m4.inverse(world)));
gl.uniformMatrix4fv(u_worldViewProjectionLoc, false, m4.multiply(viewProjection, world));
// WebGPU
m4.transpose(m4.inverse(world), worldInverseTranspose);
m4.multiply(viewProjection, world, worldViewProjection);
v3.normalize([1, 8, -10], lightDirection);

device.queue.writeBuffer(vsUniformBuffer, 0, vsUniformValues);
device.queue.writeBuffer(fsUniformBuffer, 0, fsUniformValues);

调整绘制缓冲区大小

WebGL 只需设置 canvas.width 和 canvas.height 即可;WebGPU 需要手动管理纹理和深度缓冲的销毁与重建。

// WebGL
function resizeCanvasToDisplaySize(canvas) {
  const width = canvas.clientWidth;
  const height = canvas.clientHeight;
  const needResize = width !== canvas.width || height !== canvas.height;
  if (needResize) {
    canvas.width = width;
    canvas.height = height;
  }
  return needResize;
}
// WebGPU 简化
function resizeToDisplaySize(device, canvasInfo) {
  const { canvas, context, renderTarget, presentationFormat, depthTexture, sampleCount } = canvasInfo;
  const width = Math.max(1, Math.min(device.limits.maxTextureDimension2D, canvas.clientWidth));
  const height = Math.max(1, Math.min(device.limits.maxTextureDimension2D, canvas.clientHeight));
  const needResize = !canvasInfo.renderTarget || width !== canvas.width || height !== canvas.height;
  if (needResize) {
    if (renderTarget) renderTarget.destroy();
    if (depthTexture) depthTexture.destroy();
    canvas.width = width;
    canvas.height = height;
    if (sampleCount > 1) {
      const newRenderTarget = device.createTexture({
        size: [canvas.width, canvas.height],
        format: presentationFormat,
        sampleCount,
        usage: GPUTextureUsage.RENDER_ATTACHMENT,
      });
      canvasInfo.renderTarget = newRenderTarget;
      canvasInfo.renderTargetView = newRenderTarget.createView();
    }
    const newDepthTexture = device.createTexture({
      size: [canvas.width, canvas.height],
      format: 'depth24plus',
      sampleCount,
      usage: GPUTextureUsage.RENDER_ATTACHMENT,
    });
    canvasInfo.depthTexture = newDepthTexture;
    canvasInfo.depthTextureView = newDepthTexture.createView();
  }
  return needResize;
}

WebGPU 还需要检查硬件限制(如最大纹理尺寸),并自行处理内存不足的情况(可通过 pushErrorScope('out-of-memory') 异步捕获)。

绘制

// WebGL 简化
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 绑定纹理、缓冲区、设置 uniform 等
gl.drawElements(gl.TRIANGLES, 6 * 6, gl.UNSIGNED_SHORT, 0);
// WebGPU
if (canvasInfo.sampleCount === 1) {
  const colorTexture = context.getCurrentTexture();
  renderPassDescriptor.colorAttachments[0].view = colorTexture.createView();
} else {
  renderPassDescriptor.colorAttachments[0].view = canvasInfo.renderTargetView;
  renderPassDescriptor.colorAttachments[0].resolveTarget = context.getCurrentTexture().createView();
}
renderPassDescriptor.depthStencilAttachment.view = canvasInfo.depthTextureView;

const commandEncoder = device.createCommandEncoder();
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.setVertexBuffer(0, positionBuffer);
passEncoder.setVertexBuffer(1, normalBuffer);
passEncoder.setVertexBuffer(2, texcoordBuffer);
passEncoder.setIndexBuffer(indicesBuffer, 'uint16');
passEncoder.drawIndexed(indices.length);
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);

WebGPU 中需要更新渲染通道描述符的纹理视图,创建命令编码器(command encoder),开始渲染通道,设置管线、绑定组、顶点缓冲区、索引缓冲区,最后提交命令队列。注意命令队列(device.queue):所有上传和提交都通过队列完成,不能在同一个命令编码器内部的两次绘制之间修改缓冲区。

绘制多个物体

在 WebGPU 中,绘制多个物体通常需要为每个物体创建独立的 uniform 缓冲区(以及对应的绑定组),因为 uniform 缓冲区无法在命令编码器中间更新。

// WebGPU 多物体初始化简化
const numObjects = 100;
const objectInfos = [];
for (let i = 0; i < numObjects; ++i) {
  const vsUniformBuffer = device.createBuffer({
    size: Math.max(16, vUniformBufferSize),
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
  });
  const vsUniformValues = new Float32Array(2 * 16);
  const worldViewProjection = vsUniformValues.subarray(0, 16);
  const worldInverseTranspose = vsUniformValues.subarray(16, 32);
  const bindGroup = device.createBindGroup({
    layout: pipeline.getBindGroupLayout(0),
    entries: [
      { binding: 0, resource: vsUniformBuffer },
      { binding: 1, resource: fsUniformBuffer },
      { binding: 2, resource: sampler },
      { binding: 3, resource: tex },
    ],
  });
  // 计算位置并保存引用
  objectInfos.push({ vsUniformBuffer, vsUniformValues, worldViewProjection, worldInverseTranspose, bindGroup, translation: [x, y, 0] });
}

渲染循环中,为每个物体更新其 uniform 缓冲区并编码绘制命令:

// WebGPU 渲染循环简化
passEncoder.setPipeline(pipeline);
passEncoder.setVertexBuffer(0, positionBuffer);
passEncoder.setVertexBuffer(1, normalBuffer);
passEncoder.setVertexBuffer(2, texcoordBuffer);
passEncoder.setIndexBuffer(indicesBuffer, 'uint16');

const projection = mat4.perspective(...);
const view = mat4.lookAt(...);
const viewProjection = mat4.multiply(projection, view);
vec3.normalize([1, 8, -10], lightDirection);
device.queue.writeBuffer(fsUniformBuffer, 0, fsUniformValues);

objectInfos.forEach(({ vsUniformBuffer, vsUniformValues, worldViewProjection, worldInverseTranspose, bindGroup, translation }, ndx) => {
  passEncoder.setBindGroup(0, bindGroup);
  const world = mat4.translation(translation);
  mat4.rotateX(world, time * 0.9 + ndx, world);
  mat4.rotateY(world, time + ndx, world);
  mat4.transpose(mat4.inverse(world), worldInverseTranspose);
  mat4.multiply(viewProjection, world, worldViewProjection);
  device.queue.writeBuffer(vsUniformBuffer, 0, vsUniformValues);
  passEncoder.drawIndexed(indices.length);
});
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);

核心要点:与 WebGL 不同,WebGPU 中每个物体特定的 uniform 都需要独立的 uniform 缓冲区,并因此可能需要独立的绑定组。

其他细节差异

  • Z 裁剪空间:WebGL 为 -1 到 +1;WebGPU 为 0 到 1。
  • 帧缓冲/视口坐标系 Y 轴向下:与 WebGL 相反,尽管裁剪空间中 Y 轴向上(相同)。
  • WGSL 内建变量:@builtin(position) 对应 gl_Position;@builtin(vertex_index) 对应 gl_VertexID;@builtin(instance_index) 对应 gl_InstanceID;@builtin(position) 也用于片元着色器中的 gl_FragCoord,但方向与 WebGL 相反(左上为原点)。
  • 点与线的宽度:WebGPU 只支持 1 像素宽的点与线,事实上大多数 WebGL 实现也不支持更宽的线,但 WebGL 通常支持更大的点(有 64 像素限制)。WebGPU 强制这一限制有利于编写可移植的解决方案。
  • WebGPU 优化方式不同于 WebGL:直接将 WebGL 应用转换为 WebGPU 可能运行更慢。要获得 WebGPU 的性能优势,需要重新组织数据并优化绘制方式(参见优化文章)。

三个核心优化要点

基于本文内容,从 WebGL 迁移到 WebGPU 时,以下三个优化方向尤为关键:

  1. 利用管线不可变性,预创建并复用管线
    WebGPU 渲染管线(pipeline)创建成本高,且不可修改。不同顶点布局、图元类型、混合/深度设置都需要独立管线。应在初始化阶段预先创建所有需要的管线变体,避免在渲染循环中动态创建。

  2. 显式管理资源生命周期与内存布局,避免运行时重新分配
    缓冲区、纹理的大小和格式不可变,mipmap 需自行生成,uniform 数据必须按字节偏移手动布局。应预先分配足够大小的缓冲区,使用池化或提前销毁重建的方式管理资源,同时保持 WGSL 与 JavaScript 之间的位置/偏移同步,减少运行时开销。

  3. 重新组织绘制数据,减少绑定组与命令提交开销
    WebGPU 中每个物体特有的 uniform 需要独立的缓冲区并对应独立的绑定组。应尽量合并共享数据(如光照方向、投影矩阵),并考虑使用一个大块 uniform buffer 配合动态偏移,或使用存储缓冲区(storage buffer)批量处理,以降低绑定组切换和命令队列提交的次数。


GLSL/WebGL 专业术语检查

翻译过程中涉及的核心术语均按行业标准处理:

英文术语中文翻译备注
vertex shader顶点着色器
fragment shader片元着色器亦译“片段着色器”
compute shader计算着色器
uniform统一变量 / 统一值常见音译“uniform”
attribute顶点属性
varying可变变量 / 插值变量
texture纹理
sampler采样器
buffer缓冲区
render pipeline渲染管线
render pass渲染通道
bind group绑定组
depth buffer深度缓冲
stencil buffer模板缓冲
blending混合
mipmap多级渐远纹理 / mipmap保留英文更常见
framebuffer帧缓冲
viewport视口
draw call绘制调用
command encoder命令编码器
uniform block统一变量块
location位置(索引)
byte offset字节偏移
topology图元拓扑
cull mode剔除模式
multisample多重采样
resolve解析

所有代码中的英文注释与标识符保持原样,以符合技术文章惯例。