5月28日 01:35

WebGL 中的 Shader 是什么?顶点着色器和片段着色器有什么区别?

Shader 是什么

Shader(着色器)是运行在 GPU 上的小型程序,负责控制图形渲染管线的各个阶段。WebGL 使用 GLSL(OpenGL Shading Language)编写着色器代码,一个最小的 WebGL 渲染程序必须包含两个着色器:顶点着色器和片段着色器。

理解 Shader 的关键在于搞清楚渲染管线的数据流向:JavaScript 将顶点数据传入 GPU → 顶点着色器逐顶点处理 → 图元装配与光栅化 → 片段着色器逐像素着色 → 最终输出到屏幕。

顶点着色器(Vertex Shader)

顶点着色器是渲染管线的第一个可编程阶段,对每个顶点执行一次。它的核心职责是坐标变换——将模型空间的 3D 坐标经过一系列矩阵变换映射到裁剪空间。

输入与输出

  • attribute:每个顶点独有的数据,如位置(a_position)、法线(a_normal)、纹理坐标(a_texCoord)。只在顶点着色器中可用。
  • uniform:对所有顶点统一的只读数据,如变换矩阵、光照参数。两种着色器都能访问。
  • 输出 gl_Position:裁剪空间中的顶点位置,这是顶点着色器必须设置的内置变量。
  • varying:向片段着色器传递的插值数据,如颜色、纹理坐标。GPU 会在顶点之间自动对 varying 变量做线性插值。

代码示例

glsl
attribute vec3 a_position; attribute vec2 a_texCoord; attribute vec3 a_normal; uniform mat4 u_modelMatrix; uniform mat4 u_viewMatrix; uniform mat4 u_projectionMatrix; varying vec2 v_texCoord; varying vec3 v_normal; void main() { // MVP 变换:模型空间 → 世界空间 → 观察空间 → 裁剪空间 mat4 mvp = u_projectionMatrix * u_viewMatrix * u_modelMatrix; gl_Position = mvp * vec4(a_position, 1.0); // 传递插值数据给片段着色器 v_texCoord = a_texCoord; v_normal = mat3(u_modelMatrix) * a_normal; }

关键细节

  • gl_Positionvec4 类型,第四个分量 w 用于透视除法,硬件会自动执行 gl_Position.xyz / gl_Position.w 得到 NDC 坐标。
  • 法线变换不能直接用模型矩阵,应使用模型矩阵的逆转置矩阵(transpose(inverse(mat3(u_modelMatrix)))),在存在非均匀缩放时尤其重要。
  • 如果不需要某些顶点,可以将 gl_Position.w 设为负值使其被裁剪掉,这比条件分支效率更高。

片段着色器(Fragment Shader)

片段着色器是渲染管线的最后一个可编程阶段,对光栅化后产生的每个片段(可理解为潜在像素)执行一次。它的核心职责是计算每个像素的最终颜色。

输入与输出

  • varying:从顶点着色器插值而来的数据。同名 varying 变量在两个着色器中声明后,GPU 会自动完成插值传递。
  • uniform:纹理采样器(sampler2D)、材质参数、光照信息等。
  • 输出:WebGL 1.0 中使用内置变量 gl_FragColorvec4);WebGL 2.0 中可自定义输出变量 out vec4 fragColor

代码示例

glsl
precision mediump float; varying vec2 v_texCoord; varying vec3 v_normal; uniform sampler2D u_texture; uniform vec3 u_lightDir; uniform float u_opacity; void main() { // 纹理采样 vec4 texColor = texture2D(u_texture, v_texCoord); // 简单的漫反射光照 vec3 normal = normalize(v_normal); float diff = max(dot(normal, normalize(u_lightDir)), 0.0); vec3 diffuse = diff * texColor.rgb; // 环境光 + 漫反射 vec3 ambient = 0.1 * texColor.rgb; vec3 finalColor = ambient + diffuse; gl_FragColor = vec4(finalColor, texColor.a * u_opacity); }

关键细节

  • precision mediump float 声明浮点精度,移动端推荐 mediumplowp 以提升性能,桌面端可用 highp
  • 片段着色器执行次数远多于顶点着色器(一个三角形 3 个顶点可能产生数千个片段),因此片段着色器的性能优化通常影响更大。
  • WebGL 2.0 支持多渲染目标(MRT),可同时输出多个颜色附件,用于延迟渲染等高级技术。

顶点着色器 vs 片段着色器

维度顶点着色器片段着色器
执行粒度每个顶点执行一次每个片段执行一次
核心职责坐标变换、顶点属性计算像素颜色计算、纹理采样
必需输出gl_Positiongl_FragColor(WebGL1)/ 自定义 out(WebGL2)
典型操作矩阵乘法、顶点光照纹理采样、逐像素光照、Alpha 混合
性能影响顶点数多时显著通常性能瓶颈所在
精度建议通常使用 highp移动端优先 mediump
数据来源attribute + uniformvarying + uniform

varying 变量的插值机制

varying 是两个着色器之间的核心通信桥梁,理解它的插值机制对正确使用 Shader 至关重要。

GPU 在光栅化阶段对顶点着色器输出的 varying 值做重心坐标插值。例如一个三角形三个顶点输出 v_color 分别为红、绿、蓝,那么三角形内部任意一点的 v_color 就是三个顶点颜色的加权混合,权重由该点的重心坐标决定。

需要注意:

  • 透视校正插值:WebGL 默认做透视正确的插值,这是硬件自动完成的,开发者无需手动处理。但在某些特殊场景(如屏幕空间效果)下需要理解这一机制。
  • 整型 varying:WebGL 2.0 支持 flat 限定符,禁用插值,只取provoking vertex的值,适用于传递整型数据或不需要平滑过渡的场景。

着色器编译与链接流程

WebGL 中使用着色器的完整流程如下:

javascript
// 1. 创建着色器对象 const vertexShader = gl.createShader(gl.VERTEX_SHADER); const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); // 2. 绑定源码 gl.shaderSource(vertexShader, vertexSource); gl.shaderSource(fragmentShader, fragmentSource); // 3. 编译(必须检查编译错误) gl.compileShader(vertexShader); gl.compileShader(fragmentShader); if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(vertexShader)); } // 4. 创建程序并附加着色器 const program = gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); // 5. 链接程序(也需检查错误) gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { console.error(gl.getProgramInfoLog(program)); } // 6. 使用程序 gl.useProgram(program);

实际开发中务必检查 COMPILE_STATUSLINK_STATUS,否则着色器错误会被静默吞掉,极难排查。

常见面试追问及解答

Q:为什么说片段着色器通常是性能瓶颈?

因为片段数量远大于顶点数量。一个 1920x1080 的屏幕有约 200 万像素,而一个场景的顶点数通常在几千到几万。片段着色器每帧执行的次数可能是顶点着色器的上百倍,因此减少片段着色器的计算量对整体性能影响更大。

Q:attribute 和 uniform 有什么区别?

attribute 是逐顶点变化的数据,每个顶点可以有不同的值(如位置、颜色),通过顶点缓冲区传入。uniform 是全局统一的数据,所有顶点或片段共享同一个值(如变换矩阵、光照方向),通过 gl.uniform*() 设置。attribute 只能在顶点着色器中声明,uniform 在两种着色器中都能使用。

Q:WebGL 1.0 和 WebGL 2.0 的着色器有什么主要区别?

WebGL 2.0 基于 OpenGL ES 3.0,着色器语言从 GLSL ES 1.0 升级到 GLSL ES 3.0。主要变化包括:attribute/varying 改为 in/out 语法;支持多渲染目标(MRT);新增 transform feedback 可将顶点着色器输出回写到缓冲区;支持 3D 纹理和更多纹理单元;gl_FragColor 替换为自定义输出变量。编写 WebGL 2.0 着色器需在首行声明 #version 300 es

Q:如何在着色器中调试?

GPU 着色器无法直接打断点调试。常用的调试方法有:将变量值映射到颜色输出可视化;使用 gl.getShaderInfoLog 检查编译错误;借助浏览器的 Spector.js 扩展捕获和分析每一帧的 WebGL 调用;对于复杂逻辑,先在 CPU 端用 JavaScript 实现验证后再移植到 GLSL。

标签:WebGL