标签

WebGL

WebGL(Web Graphics Library)是一种 JavaScript API,用于在任何兼容的网页浏览器中不使用插件渲染2D和3D图形。它是基于OpenGL ES的规范,旨在在Web平台上提供OpenGL的性能和功能。通过 WebGL,开发者可以为网页应用程序创建复杂的可视化效果、游戏、可视化数据和各种交互式图形体验。

WebGL
查看更多相关内容
服务端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 切换次数。
服务端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,优先解决这两个问题通常就能获得显著提升。