服务端5月29日 22:35
WebGL 缓冲区(Buffer)是什么?VBO 和 VAO 有什么区别?WebGL 缓冲区就是 GPU 显存中的一块区域,用来存顶点数据(位置、颜色、法线、UV 等)。VBO(Vertex Buffer Object)是存数据的容器,VAO(Vertex Array Object)是记录"哪个 VBO 绑到哪个 attribute、偏移量多少、步长多少"的配置快照。WebGL 1 没有 VAO(需扩展 `OES_vertex_array_object`),WebGL 2 原生支持。有了 VAO,切换绘制对象只需 `gl.bindVertexArray(vao)` 一行,不用重复设置一堆 `vertexAttribPointer`。
## 追问
### VAO 具体记录了哪些状态?
每个 attribute 的启用状态(`enableVertexAttribArray`)、绑定的 VBO(`vertexAttribPointer` 时的 `ARRAY_BUFFER`)、数据偏移和步长、以及 `ELEMENT_ARRAY_BUFFER` 的绑定。不记录 `ARRAY_BUFFER` 本身的绑定——这点容易搞混。
### 为什么 WebGL 1 没有 VAO?
OpenGL ES 2.0 规范没包含 VAO,它从 OpenGL ES 3.0 / WebGL 2 才成为标准。WebGL 1 可以用扩展 `OES_vertex_array_object`,但不是所有设备都支持。项目兼容性要求高的话,自己封装一个 VAO 管理器,内部用数组存 attribute 配置,绑定时批量调用 `vertexAttribPointer`。
### EBO(IBO)和 VBO 什么关系?
EBO(Element Buffer Object)也叫 IBO,存索引数据,告诉 GPU 按什么顺序读顶点,实现顶点复用(一个正方体 8 个顶点而非 36 个)。EBO 绑定到 `ELEMENT_ARRAY_BUFFER`,绘制时用 `gl.drawElements` 而非 `gl.drawArrays`。EBO 的绑定状态记录在当前 VAO 里。
### 什么场景必须手动管理 Buffer?
动态更新的数据(粒子系统、变形动画)需要 `gl.bufferData` 分配大小后用 `gl.bufferSubData` 局部更新,避免每帧重新分配显存。静态数据(模型网格)创建一次即可,设 `gl.STATIC_DRAW` 提示驱动放显存。
### 多个 VBO 怎么组织到同一个 VAO?
同一个 VAO 绑定期间,依次 `bindBuffer` + `vertexAttribPointer` 注册每个 VBO 到不同的 attribute location。也可以把所有数据交错打包到一个 VBO 里(interleaved),用 stride 和 offset 描述布局,减少 buffer 切换次数。标签
WebGL
WebGL(Web Graphics Library)是一种 JavaScript API,用于在任何兼容的网页浏览器中不使用插件渲染2D和3D图形。它是基于OpenGL ES的规范,旨在在Web平台上提供OpenGL的性能和功能。通过 WebGL,开发者可以为网页应用程序创建复杂的可视化效果、游戏、可视化数据和各种交互式图形体验。

服务端5月29日 22:35
WebGL 雾效(Fog)是如何实现的?WebGL 雾效的本质就是根据片段到相机的距离,在物体颜色和雾颜色之间做插值:`finalColor = mix(fogColor, objectColor, fogFactor)`。三种计算 fogFactor 的方式:线性雾 `clamp((end - dist) / (end - start), 0, 1)` 需要指定起止距离;指数雾 `exp(-density * dist)` 更自然,一个 density 参数搞定;指数平方雾 `exp(-(density*dist)²)` 过渡更柔和。深度值从视图空间的 `-viewPos.z` 或 `length(viewPos.xyz)` 获取,后者基于实际距离而非仅 Z 值,物体旋转时效果更稳定。
## 追问
### 线性雾和指数雾怎么选?
线性雾可控性强,适合有明确近远边界的场景(如走廊)。指数雾只需一个 density 参数,远处自然消融,户外场景首选。指数平方雾过渡最柔和,但远处会突然消失,实际项目很少用。
### 雾颜色一定要和背景色一致吗?
是的,否则远处的物体会被雾染成另一个颜色,而不是"融入背景"。雾颜色 = 清屏颜色 = 天空盒颜色,三者必须统一。
### 雾效能用来做性能优化吗?
可以。远处的物体被雾覆盖后几乎看不见,可以降低远处物体的 LOD 级别甚至不渲染,雾正好遮住裁剪的接缝——这是开放世界游戏的常用技巧。
### Three.js 里的 Fog 和 FogExp2 有什么区别?
`THREE.Fog(color, near, far)` 是线性雾,`THREE.FogExp2(color, density)` 是指数雾。设置 `scene.fog = new THREE.Fog(...)` 后所有材质自动应用,不需要改着色器。自定义 ShaderMaterial 需要手动读取 `fogColor`/`fogDensity`/`fogFar`/`fogNear` uniform。
### 如何实现高度雾(Height Fog)?
标准雾只看距离,高度雾额外考虑世界空间 Y 坐标:低处雾浓、高处雾淡。片段着色器中用 `worldPos.y` 做第二次混合,两个因子相乘就是最终雾浓度。服务端5月29日 22:14
WebGL Cubemap 立方体贴图是什么?有哪些应用场景?Cubemap 是 6 张正方形图片拼成的纹理盒子,用 3D 方向向量采样——GPU 根据向量哪个分量绝对值最大决定落在哪个面上,再换算成 2D 坐标取色。核心用途:天空盒、环境反射(`reflect`)、环境折射(`refract`)、菲涅尔效果。6 张图必须同尺寸且为 2 的幂次方,采样前务必设 `CLAMP_TO_EDGE` 防接缝。
## 追问
### 天空盒为什么必须去掉视图矩阵的平移分量?
天空盒模拟无限远的环境,如果跟着相机平移,走两步就穿帮了。只保留旋转:`mat4 rotOnly = mat4(mat3(viewMatrix))`。
### reflect 和 refract 的区别?
`reflect(I, N)` 计算反射方向——入射光弹回来,用于镜面/金属。`refract(I, N, eta)` 计算折射方向——光穿过透明介质弯折,用于玻璃/水。真实材质两者同时存在,用菲涅尔公式混合:正面看折射为主,侧面看反射为主。
### 动态环境映射性能开销大怎么办?
每帧渲染 6 个面代价太高。常用优化:降低分辨率(64×64 够了,反射本身就模糊)、降低更新频率(每 5-10 帧更新一次)、只给关键物体开动态反射。静态场景用预过滤环境贴图(Prefiltered Env Map),运行时零计算。
### Cubemap 接缝怎么处理?
99% 是忘了设 `CLAMP_TO_EDGE`。设了还有缝,检查 6 张图边缘像素是否连续——很多在线生成工具会在接缝处偏移 1 像素。+Y 面图片经常上下颠倒,用 `gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)` 翻转。
### Cubemap 和 Equirectangular(经纬度贴图)怎么选?
Cubemap 6 张图,GPU 采样效率高,PBR 管线原生支持。Equirectangular 一张图,存储方便但两极有拉伸畸变,采样需要三角函数计算,性能差。实际工作流:用 Equirectangular 存储/传输,运行时转换为 Cubemap 使用。服务端5月29日 01:40
WebGL 中的阴影(Shadow)是如何实现的?WebGL 阴影的主流实现方式是阴影贴图(Shadow Mapping):先从光源视角渲染场景生成深度图,再从相机视角渲染时将每个片段变换到光源空间,比较其深度与深度图中的值——若片段深度更大则处于阴影中。软阴影通过 PCF(Percentage Closer Filtering)对深度图多次采样取平均实现。两大典型问题:阴影痤疮(shadow acne)由深度精度误差导致,用基于法线-光线夹角的 bias 修复;彼得潘效应(peter panning)由 bias 过大导致阴影脱离物体,改用正面剔除渲染深度图解决。大场景用级联阴影贴图(CSM)按距离分配不同分辨率。
## 追问
- 为什么阴影贴图会产生阴影痤疮?bias 值如何根据表面法线和光线方向动态计算?
- PCF 软阴影的采样半径越大越模糊,但会带来什么性能问题?Poisson Disk 采样有何优势?
- 级联阴影贴图(CSM)如何划分级联?级联接缝处的接缝问题怎么处理?
- 点光源阴影为什么用立方体贴图?渲染立方体阴影贴图需要几次 draw call?
- WebGL 1.0 没有深度纹理附件,如何用 RGBA 编码深度值?精度损失如何补偿?
## 写段代码
```glsl
// PCF 软阴影核心逻辑
float calcShadow(vec4 lightPos, sampler2D shadowMap, vec3 normal, vec3 lightDir) {
vec3 proj = lightPos.xyz / lightPos.w * 0.5 + 0.5;
float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);
float shadow = 0.0;
vec2 texel = 1.0 / vec2(textureSize(shadowMap, 0));
for (int x = -1; x <= 1; x++)
for (int y = -1; y <= 1; y++) {
float d = texture(shadowMap, proj.xy + vec2(x,y)*texel).r;
shadow += proj.z - bias > d ? 1.0 : 0.0;
}
return shadow / 9.0;
}
```服务端5月29日 01:39
WebGL 是什么?它与 OpenGL 有什么关系?WebGL 是基于 OpenGL ES 的浏览器端 3D 图形 API,关系链为 OpenGL → OpenGL ES → WebGL。WebGL 继承了 OpenGL ES 的可编程渲染管线,通过 GLSL 编写顶点着色器和片段着色器控制 GPU 渲染,但运行在浏览器沙箱中,用 JavaScript 调用,且去除了 OpenGL 的固定管线。核心渲染流程:顶点数据经顶点着色器变换 → 图元装配 → 光栅化 → 片段着色器着色 → 帧缓冲输出。WebGL 1.0 基于 OpenGL ES 2.0,WebGL 2.0 基于 OpenGL ES 3.0。
## 追问
- WebGL 和 Canvas 2D 的本质区别是什么?Canvas 2D 能调用 GPU 吗?
- 顶点缓冲对象(VBO)的作用是什么?为什么不用 CPU 每帧传顶点数据?
- WebGL 的统一变量(uniform)和属性变量(attribute)分别在什么阶段使用?
- WebGL 2.0 相比 1.0 新增了哪些关键能力?VAO 和 3D 纹理对开发有什么影响?
- 浏览器如何保证 WebGL 的安全性?哪些 OpenGL ES 特性被有意移除了?
## 写段代码
```javascript
// WebGL 初始化:创建着色器程序并绘制三角形
const gl = canvas.getContext('webgl');
const vs = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs, 'attribute vec2 a_pos; void main(){ gl_Position=vec4(a_pos,0,1); }');
gl.compileShader(vs);
const fs = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs, 'precision mediump float; void main(){ gl_FragColor=vec4(1,0,0,1); }');
gl.compileShader(fs);
const prog = gl.createProgram();
gl.attachShader(prog, vs); gl.attachShader(prog, fs);
gl.linkProgram(prog); gl.useProgram(prog);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0,0.5, -0.5,-0.5, 0.5,-0.5]), gl.STATIC_DRAW);
const loc = gl.getAttribLocation(prog, 'a_pos');
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 3);
```服务端5月28日 02:34
WebGL 渲染管线的工作流程是什么?WebGL 渲染管线是将 3D 顶点数据转化为屏幕像素的一系列处理阶段,分为**应用程序阶段(CPU)**和 **GPU 管线阶段**两大部分。其中 GPU 管线又包含**可编程阶段**和**固定功能阶段**,面试中常围绕"哪些阶段可编程、各阶段输入输出是什么"展开追问。
## 管线总览
```
CPU 应用程序阶段
│ 提交绘制命令、设置状态
▼
顶点着色器(可编程)
▼
图元装配 + 裁剪(固定)
▼
光栅化(固定)
▼
片段着色器(可编程)
▼
逐片段测试与混合(固定)
▼
帧缓冲区
```
关键点:整条管线中**只有顶点着色器和片段着色器是可编程的**,其余阶段由 GPU 硬件固定执行。WebGL 2.0 新增了变换反馈(Transform Feedback),可以将顶点着色器的输出回收到缓冲区,实现 GPU 端的粒子计算等效果。
## 一、应用程序阶段(CPU 端)
这是开发者通过 JavaScript 控制的阶段,主要负责:
- **可见性判断**:视锥体剔除、遮挡剔除,只提交可见物体给 GPU
- **准备几何数据**:将顶点位置、法线、UV、颜色等属性写入缓冲区
- **设置渲染状态**:绑定着色器程序、设置 uniform 变量、切换纹理
- **发起绘制调用**:`gl.drawArrays()` 或 `gl.drawElements()`
这一阶段的性能瓶颈通常在 draw call 数量,合并网格和使用实例化渲染(`gl.drawArraysInstanced`)是核心优化手段。
## 二、顶点着色器(可编程阶段)
顶点着色器对每个顶点执行一次,是管线的第一个可编程阶段。
**输入**:
- 顶点属性(`attribute`):位置、法线、UV、颜色
- 全局变量(`uniform`):变换矩阵、光照参数
**核心处理 — MVP 矩阵变换**:
```glsl
attribute vec3 a_position;
attribute vec2 a_texCoord;
uniform mat4 u_model; // 模型矩阵:模型空间 → 世界空间
uniform mat4 u_view; // 视图矩阵:世界空间 → 观察空间
uniform mat4 u_projection; // 投影矩阵:观察空间 → 裁剪空间
varying vec2 v_texCoord;
void main() {
vec4 worldPos = u_model * vec4(a_position, 1.0);
vec4 viewPos = u_view * worldPos;
gl_Position = u_projection * viewPos;
v_texCoord = a_texCoord;
}
```
**输出**:裁剪空间坐标(Clip Space),`gl_Position` 的四个分量 (x, y, z, w) 中 w 用于后续透视除法。
面试追问:为什么用四维齐次坐标?——因为透视投影需要 w 分量来做透视除法,将裁剪空间转为 NDC;平移变换也需要齐次坐标才能用矩阵乘法表示。
## 三、图元装配与裁剪(固定阶段)
**图元装配**:将顶点按绘制模式(`gl.TRIANGLES`、`gl.LINES`、`gl.POINTS`)组装成图元。
**裁剪**:丢弃完全在视锥体外的图元,裁剪部分在视锥体内的图元(可能产生新顶点)。
**透视除法**:将裁剪坐标除以 w 分量,得到标准化设备坐标(NDC),x/y/z 范围均为 [-1, 1]。
**视口变换**:将 NDC 坐标映射到屏幕坐标,由 `gl.viewport(x, y, width, height)` 控制。
## 四、光栅化(固定阶段)
光栅化是将几何图元转换为片段(Fragment)的过程:
1. **三角形遍历**:检查哪些像素被三角形覆盖
2. **插值计算**:顶点属性(颜色、UV、法线)在片段间线性插值,透视校正插值由硬件自动完成
3. **生成片段**:每个被覆盖的像素生成一个片段,携带插值后的属性和深度值
片段不同于像素——片段是候选像素,还需要通过后续测试才能写入帧缓冲。
## 五、片段着色器(可编程阶段)
片段着色器对每个片段执行一次,是管线的第二个可编程阶段。
**输入**:插值后的顶点属性(`varying`)、纹理采样器(`uniform sampler2D`)
**处理**:纹理采样、光照计算、颜色混合
**输出**:最终颜色值,写入 `gl_FragColor`
```glsl
precision mediump float;
varying vec2 v_texCoord;
uniform sampler2D u_texture;
void main() {
gl_FragColor = texture2D(u_texture, v_texCoord);
}
```
## 六、逐片段操作(固定阶段)
片段着色器输出的颜色需要通过一系列测试才能写入帧缓冲:
| 操作 | 作用 |
|------|------|
| 模板测试 | 用模板缓冲区做掩码,限制绘制区域 |
| 深度测试 | 比较片段深度与深度缓冲区,丢弃被遮挡的片段 |
| 混合 | 将片段颜色与帧缓冲已有颜色按 alpha 值混合,实现半透明 |
| 抖动 | 用有限色深模拟更多颜色,减少色带 |
注意:深度测试默认关闭,需 `gl.enable(gl.DEPTH_TEST)` 开启。混合也需要 `gl.enable(gl.BLEND)` 并设置混合函数。
## 七、帧缓冲输出
通过所有测试的片段颜色被写入帧缓冲区。当一帧所有绘制完成后,前后缓冲区交换(双缓冲),画面显示到屏幕。
## 性能优化要点
- **减少 draw call**:合并静态网格为一次绘制;使用实例化渲染(`gl.drawArraysInstanced`)绘制大量相同几何体
- **优化顶点着色器**:MVP 矩阵在 CPU 端预计算 `projection * view * model`,不要在着色器中逐顶点相乘
- **减少过度绘制**:不透明物体从前到后绘制,利用深度测试提前丢弃被遮挡片段;半透明物体从后到前绘制
- **控制片段着色器复杂度**:移动端 GPU 是 tile-based 架构,片段着色器是性能瓶颈,避免 `discard`、复杂分支和过多纹理采样
- **纹理压缩**:使用 ASTC/ETC2 等压缩格式减少显存带宽
## WebGL 1.0 与 2.0 管线差异
| 特性 | WebGL 1.0 | WebGL 2.0 |
|------|-----------|----------|
| GLSL 版本 | 100 (GLSL ES 1.0) | 300 es (GLSL ES 3.0) |
| 变换反馈 | 不支持 | 支持,顶点着色器输出可回收到缓冲区 |
| 多重渲染目标 | 需扩展 | 原生支持 MRT |
| 3D 纹理 | 需扩展 | 原生支持 |
| 实例化渲染 | 需扩展 | 原生支持 |
| 顶点数组对象 | 需扩展 | 原生支持 VAO |
## 面试追问
**Q: WebGL 管线中哪些阶段可编程?**
顶点着色器和片段着色器。WebGL 2.0 新增变换反馈但不算独立阶段,几何着色器 WebGL 不支持。
**Q: 为什么需要透视除法?**
裁剪空间的齐次坐标 (x, y, z, w) 除以 w 后得到 NDC,使不同深度的物体正确投影到屏幕上。没有透视除法,远处的物体不会变小。
**Q: WebGL 和 OpenGL 管线的主要区别?**
WebGL 基于 OpenGL ES,去掉了几何着色器、曲面细分等着色器;运行在浏览器沙箱中,通过 JavaScript API 调用;着色器编译由浏览器驱动完成,不同浏览器可能有性能差异。服务端5月28日 01:36
WebGL 1.0 和 WebGL 2.0 有什么区别?## 核心答案
WebGL 1.0 基于 OpenGL ES 2.0,WebGL 2.0 基于 OpenGL ES 3.0,这是两者最根本的差异。从面试角度,掌握以下五个关键区别即可覆盖大部分考点:
1. **着色器语言升级**:WebGL 1.0 使用 GLSL ES 1.0(`attribute`/`varying`/`gl_FragColor`),WebGL 2.0 使用 GLSL ES 3.0(`in`/`out`/`自定义输出变量`),必须声明 `#version 300 es`。
2. **关键特性从扩展变原生**:3D 纹理、MRT(多重渲染目标)、实例化渲染、VAO(顶点数组对象)在 WebGL 1.0 中需要扩展支持,WebGL 2.0 全部原生提供。
3. **新增能力**:变换反馈(Transform Feedback)、采样器对象、UBO(统一缓冲区对象)、遮挡查询是 WebGL 2.0 独有的。
4. **纹理限制解除**:WebGL 1.0 中非2的幂次纹理(NPOT)不能使用 mipmap 和重复包裹,WebGL 2.0 完全支持。
5. **向后兼容性**:WebGL 2.0 大部分兼容 WebGL 1.0,但着色器编译规则更严格——保留字不可用作变量名、函数重载被禁止、全局变量初始化必须为常量表达式。
**追问:WebGL 2.0 的实例化渲染有什么实际意义?**
实例化渲染允许一次 Draw Call 绘制大量相同几何体但属性不同的物体。典型场景是绘制森林中的树木、草地、粒子群等。WebGL 1.0 需要通过 ANGLE_instanced_arrays 扩展实现,WebGL 2.0 原生支持 `drawArraysInstanced` 和 `drawElementsInstanced`,配合实例化属性(每实例一个矩阵或颜色),可将数千次 Draw Call 缩减为一次,帧率提升 3-5 倍。
## 版本背景与规范演进
WebGL 1.0 于 2011 年由 Khronos Group 发布,规范基于 OpenGL ES 2.0,为浏览器提供了第一套标准化的 GPU 加速图形接口。WebGL 2.0 于 2017 年正式发布,基于 OpenGL ES 3.0,继承了 ES 3.0 的全部新特性,同时保持与 WebGL 1.0 的高度兼容。
OpenGL ES 3.0 相比 2.0 的改进并非小修小补,而是对渲染管线的全面强化。从着色器语言到纹理系统、从缓冲区管理到帧缓冲操作,几乎每个环节都有提升。Khronos 为 WebGL 2.0 准备了比 1.0 大十倍的合规测试套件,大量图形驱动 bug 在此过程中被发现和修复,这也是 WebGL 2.0 稳定性显著优于 1.0 的重要原因。
## 特性对比总览
| 特性 | WebGL 1.0 | WebGL 2.0 |
|------|-----------|-----------|
| **基础规范** | OpenGL ES 2.0 | OpenGL ES 3.0 |
| **着色器版本** | GLSL ES 1.0 | GLSL ES 3.0 |
| **3D 纹理** | 需扩展 | 原生支持 |
| **2D 纹理数组** | 不支持 | 原生支持 |
| **多重渲染目标(MRT)** | 需扩展 | 原生支持 |
| **实例化渲染** | 需扩展 | 原生支持 |
| **变换反馈** | 不支持 | 支持 |
| **采样器对象** | 不支持 | 支持 |
| **顶点数组对象(VAO)** | 需扩展 | 原生支持 |
| **统一缓冲区对象(UBO)** | 不支持 | 支持 |
| **遮挡查询** | 不支持 | 支持 |
| **非2的幂次纹理** | 受限 | 完全支持 |
| **像素缓冲区对象(PBO)** | 不支持 | 支持 |
## 着色器语言差异
GLSL ES 3.0 是 WebGL 2.0 的着色器语言,相比 1.0 版本变化很大,迁移时需要逐项适配。
### 语法变化对照
| WebGL 1.0 (GLSL ES 1.0) | WebGL 2.0 (GLSL ES 3.0) | 说明 |
|--------------------------|--------------------------|------|
| `attribute` | `in` | 顶点着色器输入 |
| `varying` | `out`(顶点)/ `in`(片段) | 阶段间数据传递 |
| `gl_FragColor` | 自定义 `out` 变量 | 片段着色器输出 |
| `texture2D()` | `texture()` | 2D 纹理采样 |
| `textureCube()` | `texture()` | 立方体纹理采样 |
| 无 | `#version 300 es` | 版本声明(必需) |
### WebGL 1.0 着色器示例
```glsl
// 顶点着色器
attribute vec3 a_position;
attribute vec2 a_texCoord;
uniform mat4 u_mvpMatrix;
varying vec2 v_texCoord;
void main() {
gl_Position = u_mvpMatrix * vec4(a_position, 1.0);
v_texCoord = a_texCoord;
}
```
```glsl
// 片段着色器
precision mediump float;
varying vec2 v_texCoord;
uniform sampler2D u_texture;
void main() {
gl_FragColor = texture2D(u_texture, v_texCoord);
}
```
### WebGL 2.0 着色器示例
```glsl
#version 300 es
// 顶点着色器
in vec3 a_position;
in vec2 a_texCoord;
uniform mat4 u_mvpMatrix;
out vec2 v_texCoord;
void main() {
gl_Position = u_mvpMatrix * vec4(a_position, 1.0);
v_texCoord = a_texCoord;
}
```
```glsl
#version 300 es
precision mediump float;
in vec2 v_texCoord;
uniform sampler2D u_texture;
out vec4 fragColor;
void main() {
fragColor = texture(u_texture, v_texCoord);
}
```
### 编译规则变严的坑
迁移到 WebGL 2.0 时,着色器编译可能报一些在 1.0 中不会出现的错误:
- **保留字冲突**:`sample`、`smooth`、`round`、`inverse` 等在 ES 3.0 中成为保留字,不能用作变量名或函数名。
- **全局初始化必须是常量**:`float a = 1.0, b = a;` 这类写法不再合法,`a` 对 `b` 而言不是常量表达式。
- **函数重载被禁止**:不能定义同名不同参数的函数(如 `min(vec2, vec2)` 和 `min(float, float)` 不允许同时存在)。
- **`precision` 限定更严格**:片段着色器中仍需声明默认精度,但在 ES 3.0 中某些隐式转换不再允许。
## WebGL 2.0 核心新特性详解
### 3D 纹理与纹理数组
3D 纹理是体渲染、医学影像、烟雾模拟等场景的基础能力。WebGL 1.0 只能通过扩展勉强实现,WebGL 2.0 原生提供 `TEXTURE_3D` 目标和 `texImage3D` 接口。
```javascript
const texture3D = gl.createTexture();
gl.bindTexture(gl.TEXTURE_3D, texture3D);
gl.texImage3D(
gl.TEXTURE_3D,
0, // mipmap 级别
gl.RGBA8, // 内部格式
width, height, depth,
0, // 边框
gl.RGBA,
gl.UNSIGNED_BYTE,
volumeData
);
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE);
```
着色器中使用 `sampler3D` 采样:
```glsl
#version 300 es
uniform sampler3D u_volumeData;
void main() {
vec4 value = texture(u_volumeData, vec3(u, v, w));
}
```
纹理数组(`TEXTURE_2D_ARRAY`)则是另一种组织方式,它把多层 2D 纹理打包为一个对象,共享相同的尺寸和格式,但每层可以有不同内容。这在地形渲染(每层一种地表纹理)、精灵图集、动画帧序列中非常实用。
### 多重渲染目标(MRT)
MRT 允许片段着色器一次渲染同时输出到多个颜色附件。这是延迟渲染(Deferred Shading)的基石——一次 Pass 就能输出位置、法线、颜色等多张 G-Buffer 纹理。
```javascript
const fb = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
const attachments = [];
for (let i = 0; i < 4; i++) {
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, width, height, 0, gl.RGBA, gl.FLOAT, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, tex, 0);
attachments.push(tex);
}
gl.drawBuffers([
gl.COLOR_ATTACHMENT0,
gl.COLOR_ATTACHMENT1,
gl.COLOR_ATTACHMENT2,
gl.COLOR_ATTACHMENT3
]);
```
着色器中声明多个输出:
```glsl
#version 300 es
layout(location = 0) out vec4 gPosition;
layout(location = 1) out vec4 gNormal;
layout(location = 2) out vec4 gAlbedo;
layout(location = 3) out vec4 gSpecular;
void main() {
gPosition = vec4(fragPos, 1.0);
gNormal = vec4(normalize(fragNormal), 1.0);
gAlbedo = vec4(baseColor, 1.0);
gSpecular = vec4(specular, roughness, metallic, 1.0);
}
```
相比 WebGL 1.0 需要多次渲染 Pass 分别输出,MRT 将延迟着色的效率提升了 40% 以上。
### 实例化渲染
实例化渲染解决的是"相同几何体、不同属性"的批量绘制问题。典型场景包括:大规模植被、粒子系统、建筑群等。
```javascript
// 为每个实例准备独立的模型矩阵
const instanceMatrices = new Float32Array(instanceCount * 16);
// ... 填充每个实例的变换矩阵
const instanceBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);
gl.bufferData(gl.ARRAY_BUFFER, instanceMatrices, gl.STATIC_DRAW);
// 为矩阵的4列各设置一个属性(mat4 占4个属性位置)
for (let i = 0; i < 4; i++) {
const loc = 3 + i;
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 4, gl.FLOAT, false, 64, i * 16);
gl.vertexAttribDivisor(loc, 1); // 每实例更新一次
}
// 一次调用绘制所有实例
gl.drawArraysInstanced(gl.TRIANGLES, 0, vertexCount, instanceCount);
```
`vertexAttribDivisor` 是关键——它告诉 GPU 某个属性是逐顶点更新还是逐实例更新。设为 1 即逐实例,设为 0(默认)即逐顶点。
### 变换反馈(Transform Feedback)
变换反馈允许将顶点着色器的输出捕获到缓冲区对象中,而不经过光栅化阶段。这使得 GPU 端的粒子模拟、布料物理、几何处理成为可能——数据在 GPU 上完成计算后直接用于下一帧渲染,无需回读 CPU。
```javascript
const tf = gl.createTransformFeedback();
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
// 绑定输出缓冲区
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, outputBuffer);
gl.beginTransformFeedback(gl.POINTS);
gl.drawArrays(gl.POINTS, 0, particleCount);
gl.endTransformFeedback();
// outputBuffer 中现在包含变换后的顶点数据
// 可直接绑定到顶点属性用于下一帧渲染
```
着色器中声明要捕获的输出变量:
```javascript
// 编译链接前设置
gl.transformFeedbackVaryings(
program,
['v_outPosition', 'v_outVelocity'],
gl.SEPARATE_ATTRIBS
);
```
### 统一缓冲区对象(UBO)
UBO 将一组 uniform 变量打包到缓冲区对象中,可以跨多个着色器程序共享,且更新成本远低于逐个 `gl.uniform*` 调用。对于包含大量材质参数的场景(PBR 渲染中的相机参数、光照参数),UBO 能显著减少状态切换开销。
```javascript
const ubo = gl.createBuffer();
gl.bindBuffer(gl.UNIFORM_BUFFER, ubo);
gl.bufferData(gl.UNIFORM_BUFFER, new Float32Array([
// mat4 projection
...projectionMatrix,
// mat4 view
...viewMatrix,
// vec3 cameraPos + padding
cameraPos[0], cameraPos[1], cameraPos[2], 0
]), gl.DYNAMIC_DRAW);
// 绑定到绑定点
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, ubo);
```
着色器中通过 layout 绑定点访问:
```glsl
#version 300 es
layout(std140) uniform SceneData {
mat4 projection;
mat4 view;
vec3 cameraPos;
};
void main() {
gl_Position = projection * view * vec4(a_position, 1.0);
}
```
### 采样器对象
采样器对象将纹理采样参数(过滤模式、包裹模式)从纹理对象中分离出来。同一个纹理可以搭配不同的采样器,实现一次上传多种采样方式,避免频繁切换纹理参数。
```javascript
const sampler = gl.createSampler();
gl.samplerParameteri(sampler, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
gl.samplerParameteri(sampler, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// 绑定到纹理单元 0
gl.bindSampler(0, sampler);
```
### VAO 原生支持
顶点数组对象(VAO)将顶点属性配置(绑定哪个缓冲区、属性指针、启用状态)集中存储为一个对象。绘制时只需绑定 VAO 即可恢复全部配置,省去了逐个调用 `vertexAttribPointer` 和 `enableVertexAttribArray` 的开销。
```javascript
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
// 以下配置全部记录在 VAO 中
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(0);
gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(1);
gl.bindVertexArray(null); // 解绑
// 绘制时
gl.bindVertexArray(vao);
gl.drawArrays(gl.TRIANGLES, 0, count);
gl.bindVertexArray(null);
```
WebGL 1.0 中需要 OES_vertex_array_object 扩展才能使用 VAO,且扩展在各平台的支持程度不一。WebGL 2.0 将其纳入核心规范,保证了跨平台一致性。
## 纹理系统改进
### 非2的幂次纹理(NPOT)
WebGL 1.0 对 NPOT 纹理施加了严格限制:不能使用 mipmap,包裹模式只能是 `CLAMP_TO_EDGE`,过滤模式只能用 `NEAREST` 或 `LINEAR`。这意味着上传一张 300x200 的图片,要么手动补齐到 512x256,要么接受低质量采样。
WebGL 2.0 完全放开了这些限制。任意尺寸的纹理都可以生成 mipmap、使用 `REPEAT` 包裹、配合 `LINEAR_MIPMAP_LINEAR` 过滤。这对 UI 开发和图片展示场景是实质性的改善。
### 新纹理格式
WebGL 2.0 新增了大量纹理内部格式,包括:
- **浮点纹理**:`RGBA32F`、`RGBA16F`,用于 HDR 渲染和 GPGPU 计算。
- **深度纹理**:`DEPTH24_STENCIL8`、`DEPTH32F_STENCIL8`,阴影映射不再需要扩展。
- **整数纹理**:`RGBA8UI`、`RGBA16I`,支持逐纹素读取精确整数值。
- **压缩纹理**:支持 ETC2/EAC 压缩格式,减少显存占用和上传带宽。
### 像素缓冲区对象(PBO)
PBO 允许异步传输纹理数据。上传纹理时通过 PBO 做中转,GPU 可以在后台完成数据搬运,不阻塞主线程,对动态纹理更新(视频纹理、实时数据可视化)非常有利。
```javascript
const pbo = gl.createBuffer();
gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, pbo);
gl.bufferData(gl.PIXEL_UNPACK_BUFFER, imageData.byteLength, gl.STREAM_DRAW);
// 填充数据
gl.bufferSubData(gl.PIXEL_UNPACK_BUFFER, 0, imageData);
// 异步上传纹理
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, 0);
// 最后一个参数 0 表示从当前绑定的 PBO 读取偏移量
gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null);
```
## 帧缓冲区增强
WebGL 2.0 区分了 `DRAW_FRAMEBUFFER` 和 `READ_FRAMEBUFFER` 两个目标。这意味着可以在一个帧缓冲区上绘制,同时从另一个帧缓冲区读取,实现高效的后处理管线(如多 Pass 模糊、SSAO 等)而无需频繁切换帧缓冲区绑定。
帧缓冲区完整性检查也更加细粒度。WebGL 1.0 只有 `FRAMEBUFFER_INCOMPLETE` 之类的笼统状态,WebGL 2.0 提供了 `FRAMEBUFFER_INCOMPLETE_DIMENSIONS`、`FRAMEBUFFER_UNSUPPORTED` 等具体错误码,调试配置问题时更有方向。
## 浏览器支持与检测
截至 2026 年,所有主流浏览器均已支持 WebGL 2.0:
| 浏览器 | WebGL 1.0 | WebGL 2.0 |
|--------|-----------|-----------|
| Chrome | 全版本 | 56+ |
| Firefox | 全版本 | 51+ |
| Safari | 全版本 | 15+ |
| Edge | 全版本 | 79+ |
| IE 11 | 支持 | 不支持 |
检测代码采用渐进增强策略:
```javascript
function getWebGLContext(canvas) {
const gl2 = canvas.getContext('webgl2');
if (gl2) {
console.log('WebGL 2.0 available');
return gl2;
}
const gl1 = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (gl1) {
console.log('Falling back to WebGL 1.0');
return gl1;
}
return null;
}
```
实际项目中更推荐使用特性检测而非版本检测——先尝试创建 WebGL 2.0 上下文,检查所需的具体扩展或功能是否存在,再决定渲染路径。
## 迁移注意事项
从 WebGL 1.0 迁移到 2.0 需要关注以下几点:
**着色器适配**是最常见的工作量来源。`attribute` 换 `in`、`varying` 换 `out`/`in`、`gl_FragColor` 换自定义输出、`texture2D` 换 `texture`,这些都是机械替换,但保留字冲突和编译规则变严导致的错误需要逐个排查。
**扩展降级**需要梳理。原来依赖 `WEBGL_draw_buffers`、`OES_vertex_array_object`、`ANGLE_instanced_arrays` 等扩展的代码,在 WebGL 2.0 中应该切换到原生 API。同时要保留 WebGL 1.0 的回退路径。
**NPOT 纹理处理代码可以简化**。原来为适配 WebGL 1.0 限制而写的纹理尺寸补齐逻辑,在 WebGL 2.0 中可以去掉,但保留对 WebGL 1.0 回退路径的兼容。
**性能优化空间重新评估**。迁移完成后,应重新审视渲染管线:MRT 可以简化延迟渲染的 Pass 数量,实例化渲染可以合并同类 Draw Call,UBO 可以减少 uniform 更新开销,变换反馈可以把 CPU 端的粒子计算搬到 GPU。
WebGL 2.0 已经是成熟稳定的标准,Safari 15+ 的全面支持意味着移动端也基本覆盖。对于新项目,建议直接使用 WebGL 2.0 作为基线,WebGL 1.0 仅做降级兜底。对于已有项目,渐进迁移是最稳妥的路线——先跑通 WebGL 2.0 上下文,再逐步将扩展调用替换为原生 API,最后利用新特性优化渲染性能。服务端5月28日 01:36
WebGL 中的纹理(Texture)如何使用?有哪些纹理参数需要配置?## 纹理是 WebGL 渲染的核心机制
纹理(Texture)是将 2D 图像数据映射到 3D 几何体表面的技术。在 WebGL 中,几乎所有视觉效果——地板砖纹、角色皮肤、天空背景——都依赖纹理实现。理解纹理的使用流程和参数配置,是掌握 WebGL 的关键一步。
## 纹理使用的完整流程
### 1. 创建并绑定纹理对象
```javascript
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
```
`bindTexture` 将纹理对象绑定到当前纹理单元,后续所有纹理操作都针对该绑定对象。
### 2. 翻转 Y 轴(面试高频考点)
WebGL 纹理坐标原点在**左下角**,而图片坐标原点在**左上角**。不翻转会导致纹理倒置:
```javascript
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
```
这行代码必须写在 `texImage2D` 之前。面试中经常追问:为什么 WebGL 纹理坐标系与图片坐标系方向相反?答案在于 OpenGL 传统——纹理坐标沿用数学坐标系(Y 向上),而图片格式遵循扫描线顺序(Y 向下)。
### 3. 上传纹理数据
```javascript
// 从 Image 对象加载
gl.texImage2D(
gl.TEXTURE_2D, // 目标
0, // mipmap 级别
gl.RGBA, // 内部格式
gl.RGBA, // 源格式
gl.UNSIGNED_BYTE, // 数据类型
image // Image 对象
);
// 直接上传像素数据
gl.texImage2D(
gl.TEXTURE_2D, 0, gl.RGBA,
width, height, 0, // 宽、高、边框(必须为0)
gl.RGBA, gl.UNSIGNED_BYTE,
pixels // Uint8Array
);
```
### 4. 配置纹理参数
```javascript
// 环绕方式
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);
// 过滤方式
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
```
### 5. 生成 Mipmap(可选)
```javascript
gl.generateMipmap(gl.TEXTURE_2D);
```
Mipmap 会生成一系列逐级减半的纹理副本,在物体远离相机时使用更小的纹理,既提升渲染质量又节省带宽。内存开销仅增加约 30%。
## 纹理参数详解
### 纹理环绕方式(Texture Wrapping)
控制纹理坐标超出 [0, 1] 范围时的行为:
| 参数值 | 效果 | 典型场景 |
|--------|------|----------|
| `gl.REPEAT` | 重复平铺 | 地板砖纹、墙壁图案 |
| `gl.CLAMP_TO_EDGE` | 边缘像素延伸 | 天空盒、非重复贴图 |
| `gl.MIRRORED_REPEAT` | 镜像重复 | 对称图案贴图 |
```
REPEAT: CLAMP_TO_EDGE: MIRRORED_REPEAT:
|ABCD|ABCD| |AAAA|ABCD|DDDD| |ABCD|DCBA|ABCD|
|ABCD|ABCD| |AAAA|ABCD|DDDD| |ABCD|DCBA|ABCD|
```
**面试追问**:非 2 的幂次(NPOT)纹理只能使用 `CLAMP_TO_EDGE`,不能使用 `REPEAT`,也不能生成 Mipmap。这是 WebGL1 的重要限制,WebGL2 已解除。
### 纹理过滤方式(Texture Filtering)
#### 放大过滤(MAG_FILTER)
纹理被放大时(纹理像素 < 屏幕像素):
| 参数值 | 效果 |
|--------|------|
| `gl.NEAREST` | 最近邻采样,像素化效果,速度快 |
| `gl.LINEAR` | 双线性插值,平滑效果(推荐) |
#### 缩小过滤(MIN_FILTER)
纹理被缩小时(纹理像素 > 屏幕像素):
| 参数值 | 效果 |
|--------|------|
| `gl.NEAREST` | 最近邻采样 |
| `gl.LINEAR` | 双线性插值 |
| `gl.NEAREST_MIPMAP_NEAREST` | 选最近 mipmap 级别 + 最近采样 |
| `gl.LINEAR_MIPMAP_NEAREST` | 选最近 mipmap 级别 + 线性插值 |
| `gl.NEAREST_MIPMAP_LINEAR` | mipmap 间线性过渡 + 最近采样 |
| `gl.LINEAR_MIPMAP_LINEAR` | 三线性过滤,质量最高 |
```javascript
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
```
## 着色器中的纹理使用
### 顶点着色器
```glsl
attribute vec3 a_position;
attribute vec2 a_texCoord;
uniform mat4 u_mvpMatrix;
varying vec2 v_texCoord;
void main() {
gl_Position = u_mvpMatrix * vec4(a_position, 1.0);
v_texCoord = a_texCoord;
}
```
### 片段着色器
```glsl
precision mediump float;
varying vec2 v_texCoord;
uniform sampler2D u_texture;
void main() {
gl_FragColor = texture2D(u_texture, v_texCoord);
}
```
### JavaScript 端绑定
```javascript
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform1i(textureLocation, 0); // 对应 TEXTURE0
```
**面试追问**:`gl.uniform1i` 传入的 0 代表什么?它指定纹理单元的索引,0 对应 `gl.TEXTURE0`,1 对应 `gl.TEXTURE1`,以此类推。
## 纹理坐标系
```
(0, 1) ──────── (1, 1)
│ │
│ 纹理图像 │
│ │
(0, 0) ──────── (1, 0)
```
- 原点在**左下角**(Y 向上),与图片坐标(左上角,Y 向下)相反
- 坐标范围 [0, 1],与纹理实际像素尺寸无关
- 超出 [0, 1] 范围的行为由环绕方式决定
## 多纹理混合
WebGL 支持同时使用多个纹理,通过纹理单元(Texture Unit)管理:
```javascript
// 纹理1 绑定到 TEXTURE0
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture1);
gl.uniform1i(texture1Location, 0);
// 纹理2 绑定到 TEXTURE1
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, texture2);
gl.uniform1i(texture2Location, 1);
```
```glsl
// 片段着色器
uniform sampler2D u_texture1;
uniform sampler2D u_texture2;
varying vec2 v_texCoord;
void main() {
vec4 color1 = texture2D(u_texture1, v_texCoord);
vec4 color2 = texture2D(u_texture2, v_texCoord);
gl_FragColor = mix(color1, color2, 0.5);
}
```
## 立方体纹理(CubeMap)
CubeMap 由 6 个正方形纹理组成,分别对应立方体的 6 个面。与 2D 纹理的关键区别:
| 特性 | 2D 纹理 | CubeMap |
|------|---------|---------|
| 目标 | `gl.TEXTURE_2D` | `gl.TEXTURE_CUBE_MAP` |
| 采样器 | `sampler2D` | `samplerCube` |
| 坐标 | 二维 (s, t) | 三维方向向量 (x, y, z) |
| 采样函数 | `texture2D()` | `textureCube()` |
| 典型用途 | 表面贴图 | 环境反射、天空盒 |
```javascript
// 创建 CubeMap
const cubeTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubeTexture);
// 为六个面分别设置纹理
const faces = [
gl.TEXTURE_CUBE_MAP_POSITIVE_X,
gl.TEXTURE_CUBE_MAP_NEGATIVE_X,
gl.TEXTURE_CUBE_MAP_POSITIVE_Y,
gl.TEXTURE_CUBE_MAP_NEGATIVE_Y,
gl.TEXTURE_CUBE_MAP_POSITIVE_Z,
gl.TEXTURE_CUBE_MAP_NEGATIVE_Z,
];
faces.forEach((face, i) => {
gl.texImage2D(face, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, images[i]);
});
```
## 完整纹理加载函数
```javascript
function loadTexture(gl, url) {
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// 临时 1x1 蓝色像素,图片加载完成前使用
gl.texImage2D(
gl.TEXTURE_2D, 0, gl.RGBA,
1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
new Uint8Array([0, 0, 255, 255])
);
const image = new Image();
image.onload = function () {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
gl.texImage2D(
gl.TEXTURE_2D, 0, gl.RGBA,
gl.RGBA, gl.UNSIGNED_BYTE, image
);
if (isPowerOf2(image.width) && isPowerOf2(image.height)) {
gl.generateMipmap(gl.TEXTURE_2D);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER,
gl.LINEAR_MIPMAP_LINEAR);
} else {
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);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
}
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
};
image.src = url;
return texture;
}
function isPowerOf2(value) {
return (value & (value - 1)) === 0;
}
```
## 性能优化要点
- **纹理图集(Texture Atlas)**:将多个小纹理合并为一张大图,用不同 UV 坐标区分,减少纹理切换和绘制调用
- **优先使用 2 的幂次尺寸**:128、256、512、1024、2048,才能启用 Mipmap 和 REPEAT 环绕
- **纹理尺寸上限**:移动端保证支持 2048x2048,通过 `gl.getParameter(gl.MAX_TEXTURE_SIZE)` 查询实际限制
- **压缩纹理格式**:DXT(桌面端)、ETC(Android)、PVRTC(iOS),减少显存占用和加载时间
- **及时释放纹理**:`gl.deleteTexture(texture)` 回收 GPU 资源,避免内存泄漏
- **Mipmap 内存开销**:仅增加约 30%,但在远距离渲染时显著提升质量和性能服务端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_Position` 是 `vec4` 类型,第四个分量 `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_FragColor`(`vec4`);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` 声明浮点精度,移动端推荐 `mediump` 或 `lowp` 以提升性能,桌面端可用 `highp`。
- 片段着色器执行次数远多于顶点着色器(一个三角形 3 个顶点可能产生数千个片段),因此片段着色器的性能优化通常影响更大。
- WebGL 2.0 支持多渲染目标(MRT),可同时输出多个颜色附件,用于延迟渲染等高级技术。
## 顶点着色器 vs 片段着色器
| 维度 | 顶点着色器 | 片段着色器 |
|------|-----------|----------|
| 执行粒度 | 每个顶点执行一次 | 每个片段执行一次 |
| 核心职责 | 坐标变换、顶点属性计算 | 像素颜色计算、纹理采样 |
| 必需输出 | `gl_Position` | `gl_FragColor`(WebGL1)/ 自定义 out(WebGL2) |
| 典型操作 | 矩阵乘法、顶点光照 | 纹理采样、逐像素光照、Alpha 混合 |
| 性能影响 | 顶点数多时显著 | 通常性能瓶颈所在 |
| 精度建议 | 通常使用 `highp` | 移动端优先 `mediump` |
| 数据来源 | attribute + uniform | varying + 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_STATUS` 和 `LINK_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。服务端5月28日 01:34
WebGL 性能优化有哪些常用技巧?## 减少 Draw Call
Draw Call 是 CPU 向 GPU 提交绘制命令的过程,每次调用都有固定开销,是最常见的性能瓶颈。
**批量绘制(Batching)**:将使用相同着色器的多个网格合并到一个缓冲区,用一次 `drawArrays` 替代多次调用。适合静态场景中的同类物体。
**实例化渲染(Instanced Rendering)**:WebGL 2.0 原生支持,适合渲染大量相同几何体(如森林中的树木、粒子系统):
```javascript
gl.drawArraysInstanced(gl.TRIANGLES, 0, vertexCount, instanceCount);
```
实例化通过 `gl.vertexAttribDivisor` 让每个实例读取不同的属性(位置、颜色等),一次 Draw Call 完成。
**关键指标**:移动端 Draw Call 超过 100 次就可能卡顿,桌面端建议控制在 2000 次以内。
## 减少状态切换
每次切换着色器程序、纹理、Blend 模式等状态都会触发 GPU 管线刷新。优化思路:
- **按状态排序**:先按着色器分组,再按纹理分组,减少 `gl.useProgram` 和 `gl.bindTexture` 调用次数
- **纹理图集(Texture Atlas)**:将多张小纹理合并为一张大图,通过 UV 偏移采样,只需绑定一次纹理
- **VAO(Vertex Array Object)**:WebGL 2.0 原生支持,将顶点属性配置缓存到 VAO 中,切换时只需 `gl.bindVertexArray(vao)`,替代逐个设置 `vertexAttribPointer`
## 着色器优化
**CPU 预计算**:将 `projection * view * model` 矩阵在 JS 端乘好再传入着色器,避免每个顶点重复计算:
```glsl
uniform mat4 u_mvpMatrix;
void main() {
gl_Position = u_mvpMatrix * vec4(a_position, 1.0);
}
```
**精度控制**:移动端 GPU 对精度敏感,合理使用精度修饰符可提升 2-5 倍性能:
```glsl
highp mat4 u_mvpMatrix; // 变换矩阵必须高精度
mediump vec2 a_texCoord; // 纹理坐标中精度足够
lowp vec4 v_color; // 颜色低精度即可
```
移动端片段着色器默认 `mediump float` 即可,盲目使用 `highp` 会显著拖慢渲染。
**将计算移至顶点着色器**:光照计算从片段着色器移到顶点着色器,利用硬件插值,减少逐像素计算量。
**减少分支**:GPU 是 SIMT 架构,动态分支(`if/else`)会导致同一 warp 内线程分叉执行,性能骤降。用 `mix`、`step` 等内置函数替代条件判断。
## 几何与缓冲区优化
**索引绘制**:用 `gl.drawElements` + 索引缓冲区复用顶点。一个立方体从 36 个顶点减少到 8 个顶点 + 36 个索引,顶点数据减少约 75%。
**交错顶点数据(Interleaved Arrays)**:将 position、color、texCoord 打包到一个缓冲区,比分离缓冲区缓存命中率更高:
```javascript
// x,y,z, r,g,b, u,v 交错排列
const vertices = new Float32Array([
0,0,0, 1,0,0, 0,0,
1,0,0, 0,1,0, 1,0,
]);
```
**Draco 压缩**:对 glTF 模型使用 Google Draco 压缩,典型压缩率可达 10:1,大幅减少加载时间和内存占用。
## 纹理优化
**纹理压缩**:使用 GPU 原生支持的压缩格式(S3TC/DXT for 桌面、ETC2 for Android、ASTC for iOS),减少 75% 显存占用:
```javascript
const ext = gl.getExtension("WEBGL_compressed_texture_s3tc");
gl.compressedTexImage2D(gl.TEXTURE_2D, 0, ext.COMPRESSED_RGBA_S3TC_DXT5_EXT, w, h, 0, data);
```
**Mipmap**:对会缩小的纹理开启 Mipmap,`LINEAR_MIPMAP_LINEAR` 过滤既提升渲染质量又减少纹理采样带宽。注意 2 的幂次尺寸才能自动生成 Mipmap。
**LOD(Level of Detail)**:根据物体与相机距离切换不同精度的纹理和模型,远距离用低精度资源,近处用高精度。
## 剔除策略
**视锥体剔除(Frustum Culling)**:用包围盒做快速判定,完全在视锥外的物体直接跳过,避免提交 GPU:
```javascript
for (const obj of scene.objects) {
if (isInFrustum(obj.boundingBox, vpMatrix)) {
obj.render();
}
}
```
**背面剔除**:`gl.enable(gl.CULL_FACE)` 一行代码即可剔除背向相机的三角形,减少约 50% 的片元处理量。
**遮挡查询**:WebGL 2.0 的 `gl.beginQuery(gl.ANY_SAMPLES_PASSED, query)` 可判断物体是否被遮挡,被遮挡则跳过精细渲染。
**从前往后排序**:不透明物体按距相机由近到远绘制,利用 Early-Z 测试减少 overdraw。
## 分辨率与帧缓冲区
**DPR 限制**:高 DPI 屏幕上不必按完整 `devicePixelRatio` 渲染:
```javascript
const dpr = Math.min(window.devicePixelRatio, 2); // 上限 2x
canvas.width = canvas.clientWidth * dpr;
```
**动态降分辨率**:帧率低于阈值时自动降低渲染分辨率,用 CSS 缩放回原始尺寸,用户几乎无感知但帧率可提升 30-50%。
**延迟渲染 G-Buffer 精度**:位置用 RGBA16F,法线用 RGB10_A2,材质用 RGBA8,避免全部高精度浪费带宽。
## JavaScript 层优化
**避免 GC**:渲染循环内不创建新对象,预分配 `Float32Array` 复用:
```javascript
const matrix = new Float32Array(16);
function update() {
mat4.identity(matrix); // 复用
}
```
**OffscreenCanvas + WebWorker**:将渲染逻辑移至 Worker 线程,避免阻塞主线程 UI 响应:
```javascript
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker("renderer.js");
worker.postMessage({ canvas: offscreen }, [offscreen]);
```
**及时释放资源**:不再使用的纹理、缓冲区立即 `gl.deleteTexture` / `gl.deleteBuffer`,避免显存泄漏。
## 性能监控
**GPU 计时**:`EXT_disjoint_timer_query` 扩展可精确测量 GPU 执行时间,定位渲染瓶颈:
```javascript
const ext = gl.getExtension("EXT_disjoint_timer_query");
const query = ext.createQueryEXT();
ext.beginQueryEXT(ext.TIME_ELAPSED_EXT, query);
drawScene();
ext.endQueryEXT(ext.TIME_ELAPSED_EXT);
```
**Chrome DevTools**:Performance 面板可分析帧时间分布,判断瓶颈在 CPU(JS 逻辑)还是 GPU(渲染管线)。
**Spector.js**:浏览器扩展,逐帧记录所有 WebGL 调用,直观发现冗余状态切换和 Draw Call 问题。
## 面试回答要点
回答 WebGL 性能优化时,按优先级组织:先说 Draw Call 和状态切换(最常见瓶颈),再说着色器优化和几何优化(GPU 侧),最后说 JS 层和监控工具。关键是展示系统性思维——知道瓶颈在哪、用什么工具定位、用什么方案解决。实际项目中,90% 的性能问题来自 Draw Call 过多和 overdraw,优先解决这两个问题通常就能获得显著提升。