乐闻世界logo
搜索文章和话题

面试题手册

WebGL 渲染管线的工作流程是什么?

WebGL 渲染管线(Rendering Pipeline)是一系列将 3D 顶点数据转换为 2D 屏幕像素的处理阶段。理解渲染管线对于优化 WebGL 应用性能至关重要。渲染管线的各个阶段1. 顶点处理阶段(Vertex Processing)顶点着色器(Vertex Shader)输入:顶点位置、颜色、纹理坐标、法线等属性处理:坐标变换(模型矩阵、视图矩阵、投影矩阵)顶点光照计算纹理坐标变换输出:裁剪空间坐标(Clip Space Coordinates)// 顶点着色器示例attribute vec3 a_position;attribute vec2 a_texCoord;uniform mat4 u_modelViewProjectionMatrix;varying vec2 v_texCoord;void main() { gl_Position = u_modelViewProjectionMatrix * vec4(a_position, 1.0); v_texCoord = a_texCoord;}2. 图元装配阶段(Primitive Assembly)将顶点组装成图元(点、线、三角形)执行裁剪(Clipping):移除视锥体外的图元执行透视除法(Perspective Division):将裁剪坐标转换为标准化设备坐标(NDC)3. 光栅化阶段(Rasterization)将图元转换为片段(Fragments)片段是潜在的像素,包含颜色、深度等信息确定哪些像素被图元覆盖进行插值计算:颜色、纹理坐标、法线等属性在片段间的插值4. 片段处理阶段(Fragment Processing)片段着色器(Fragment Shader)输入:插值后的顶点属性处理:纹理采样光照计算颜色混合输出:最终像素颜色// 片段着色器示例precision mediump float;varying vec2 v_texCoord;uniform sampler2D u_texture;void main() { gl_FragColor = texture2D(u_texture, v_texCoord);}5. 逐片段操作阶段(Per-Fragment Operations)深度测试(Depth Testing)比较片段深度值与深度缓冲区决定是否丢弃片段模板测试(Stencil Testing)使用模板缓冲区进行掩码操作混合(Blending)将片段颜色与帧缓冲区已有颜色混合实现透明效果抖动(Dithering)减少颜色量化带来的色带WebGL 渲染流程图顶点数据 → 顶点着色器 → 图元装配 → 裁剪 → 透视除法 → 视口变换 ↓帧缓冲区 ← 逐片段操作 ← 片段着色器 ← 光栅化/插值 ← 屏幕映射性能优化要点减少绘制调用:合并网格,使用实例化渲染优化顶点着色器:避免复杂计算减少片段着色器复杂度:特别是移动端合理使用深度测试:从前到后绘制不透明物体避免过度绘制:使用遮挡查询WebGL 1.0 vs WebGL 2.0 管线差异| 特性 | WebGL 1.0 | WebGL 2.0 || ------ | --------- | --------- || 变换反馈 | 不支持 | 支持 || 多重渲染目标 | 需要扩展 | 原生支持 || 3D 纹理 | 需要扩展 | 原生支持 || 实例化渲染 | 需要扩展 | 原生支持 |​
阅读 0·3月7日 19:38

WebGL 中的立方体贴图(Cubemap)是什么?有哪些应用场景?

WebGL 立方体贴图概述立方体贴图(Cubemap)是一种特殊的纹理,由 6 张独立的 2D 纹理组成,分别对应立方体的 6 个面。它使用 3D 方向向量进行采样,常用于实现天空盒、环境反射和折射等效果。立方体贴图的结构立方体贴图由 6 个面组成: ┌─────────┐ │ +Y │ (Top)┌──────┼─────────┼──────┬─────────┐│ -X │ +Z │ +X │ -Z ││ Left │ Front │ Right│ Back │└──────┴─────────┴──────┴─────────┘ │ -Y │ (Bottom) └─────────┘创建立方体贴图基本创建流程// 创建立方体贴图const cubemap = gl.createTexture();gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubemap);// 6 个面的图片 URLconst faceImages = [ 'px.jpg', // +X (Right) 'nx.jpg', // -X (Left) 'py.jpg', // +Y (Top) 'ny.jpg', // -Y (Bottom) 'pz.jpg', // +Z (Front) 'nz.jpg' // -Z (Back)];// 加载 6 个面的图片const targets = [ 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];let loadedCount = 0;faceImages.forEach((src, index) => { const image = new Image(); image.onload = () => { gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubemap); gl.texImage2D( targets[index], 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image ); loadedCount++; if (loadedCount === 6) { // 所有面加载完成,生成 mipmap gl.generateMipmap(gl.TEXTURE_CUBE_MAP); } }; image.src = src;});// 设置纹理参数gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE);程序化生成立方体贴图// 创建纯色立方体贴图function createSolidColorCubemap(gl, color) { const cubemap = gl.createTexture(); gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubemap); const targets = [ 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 ]; const size = 1; const data = new Uint8Array([ color[0] * 255, color[1] * 255, color[2] * 255, (color[3] || 1) * 255 ]); targets.forEach(target => { gl.texImage2D( target, 0, gl.RGBA, size, size, 0, gl.RGBA, gl.UNSIGNED_BYTE, data ); }); return cubemap;}在着色器中使用立方体贴图顶点着色器attribute vec3 a_position;uniform mat4 u_modelMatrix;uniform mat4 u_viewMatrix;uniform mat4 u_projectionMatrix;varying vec3 v_worldPos;void main() { vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0); v_worldPos = worldPos.xyz; gl_Position = u_projectionMatrix * u_viewMatrix * worldPos;}片段着色器precision mediump float;varying vec3 v_worldPos;uniform vec3 u_cameraPos;uniform samplerCube u_cubemap;void main() { // 计算从相机指向片段的方向向量 vec3 direction = normalize(v_worldPos - u_cameraPos); // 使用方向向量采样立方体贴图 vec4 color = textureCube(u_cubemap, direction); gl_FragColor = color;}主要应用场景1. 天空盒(Skybox)天空盒用于渲染远处的环境背景,给人一种无限大的感觉。// 天空盒顶点着色器attribute vec3 a_position;uniform mat4 u_viewMatrix;uniform mat4 u_projectionMatrix;varying vec3 v_texCoord;void main() { // 移除平移分量,只保留旋转 mat4 viewRotation = mat4(mat3(u_viewMatrix)); vec4 pos = u_projectionMatrix * viewRotation * vec4(a_position, 1.0); // 确保天空盒在远裁剪面 gl_Position = pos.xyww; // 使用位置作为纹理坐标 v_texCoord = a_position;}// 天空盒片段着色器precision mediump float;varying vec3 v_texCoord;uniform samplerCube u_skybox;void main() { gl_FragColor = textureCube(u_skybox, v_texCoord);}// 渲染天空盒function drawSkybox(gl, skyboxProgram, cubemap) { // 禁用深度写入 gl.depthMask(false); gl.useProgram(skyboxProgram); // 绑定立方体贴图 gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubemap); gl.uniform1i(gl.getUniformLocation(skyboxProgram, 'u_skybox'), 0); // 绘制立方体 drawCube(gl); // 恢复深度写入 gl.depthMask(true);}2. 环境反射(Environment Reflection)使用立方体贴图模拟光滑表面的反射效果。// 反射顶点着色器attribute vec3 a_position;attribute vec3 a_normal;uniform mat4 u_modelMatrix;uniform mat4 u_viewMatrix;uniform mat4 u_projectionMatrix;varying vec3 v_worldPos;varying vec3 v_normal;void main() { vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0); v_worldPos = worldPos.xyz; v_normal = mat3(u_modelMatrix) * a_normal; gl_Position = u_projectionMatrix * u_viewMatrix * worldPos;}// 反射片段着色器precision mediump float;varying vec3 v_worldPos;varying vec3 v_normal;uniform vec3 u_cameraPos;uniform samplerCube u_environmentMap;uniform float u_reflectivity;void main() { vec3 normal = normalize(v_normal); vec3 viewDir = normalize(u_cameraPos - v_worldPos); // 计算反射向量 vec3 reflectDir = reflect(-viewDir, normal); // 采样环境贴图 vec4 reflectionColor = textureCube(u_environmentMap, reflectDir); // 可以结合基础颜色和反射 vec3 baseColor = vec3(0.8, 0.8, 0.8); vec3 finalColor = mix(baseColor, reflectionColor.rgb, u_reflectivity); gl_FragColor = vec4(finalColor, 1.0);}3. 环境折射(Environment Refraction)模拟光线穿过透明物体的折射效果。// 折射片段着色器precision mediump float;varying vec3 v_worldPos;varying vec3 v_normal;uniform vec3 u_cameraPos;uniform samplerCube u_environmentMap;uniform float u_refractiveIndex; // 折射率,如 1.52 表示玻璃void main() { vec3 normal = normalize(v_normal); vec3 viewDir = normalize(u_cameraPos - v_worldPos); // 计算折射向量 // refract(I, N, eta) 其中 eta = 入射介质折射率 / 折射介质折射率 vec3 refractDir = refract(-viewDir, normal, 1.0 / u_refractiveIndex); // 采样环境贴图 vec4 refractionColor = textureCube(u_environmentMap, refractDir); gl_FragColor = refractionColor;}4. 菲涅尔反射(Fresnel Reflection)模拟真实世界中反射强度随视角变化的效应。// 菲涅尔反射片段着色器precision mediump float;varying vec3 v_worldPos;varying vec3 v_normal;uniform vec3 u_cameraPos;uniform samplerCube u_environmentMap;uniform float u_fresnelPower;void main() { vec3 normal = normalize(v_normal); vec3 viewDir = normalize(u_cameraPos - v_worldPos); // 计算反射向量 vec3 reflectDir = reflect(-viewDir, normal); vec4 reflectionColor = textureCube(u_environmentMap, reflectDir); // 计算菲涅尔因子 // 视线与法线夹角越大,反射越强 float fresnel = pow(1.0 - max(dot(viewDir, normal), 0.0), u_fresnelPower); // 基础颜色 vec3 baseColor = vec3(0.1, 0.2, 0.3); // 混合基础颜色和反射 vec3 finalColor = mix(baseColor, reflectionColor.rgb, fresnel); gl_FragColor = vec4(finalColor, 1.0);}5. 动态环境贴图(Dynamic Environment Mapping)实时生成立方体贴图用于反射。// 动态生成环境贴图function generateEnvironmentMap(gl, scene, centerPos, size = 256) { // 创建立方体贴图 const cubemap = gl.createTexture(); gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubemap); // 创建帧缓冲区 const framebuffer = gl.createFramebuffer(); // 6 个面的相机方向 const directions = [ { target: gl.TEXTURE_CUBE_MAP_POSITIVE_X, eye: [1, 0, 0], up: [0, -1, 0] }, { target: gl.TEXTURE_CUBE_MAP_NEGATIVE_X, eye: [-1, 0, 0], up: [0, -1, 0] }, { target: gl.TEXTURE_CUBE_MAP_POSITIVE_Y, eye: [0, 1, 0], up: [0, 0, 1] }, { target: gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, eye: [0, -1, 0], up: [0, 0, -1] }, { target: gl.TEXTURE_CUBE_MAP_POSITIVE_Z, eye: [0, 0, 1], up: [0, -1, 0] }, { target: gl.TEXTURE_CUBE_MAP_NEGATIVE_Z, eye: [0, 0, -1], up: [0, -1, 0] } ]; // 设置投影矩阵(90度视野) const projectionMatrix = mat4.create(); mat4.perspective(projectionMatrix, Math.PI / 2, 1, 0.1, 1000); // 渲染 6 个面 directions.forEach(dir => { // 设置视图矩阵 const viewMatrix = mat4.create(); mat4.lookAt(viewMatrix, centerPos, [centerPos[0] + dir.eye[0], centerPos[1] + dir.eye[1], centerPos[2] + dir.eye[2]], dir.up ); // 渲染场景到帧缓冲区 renderSceneToCubemapFace(gl, scene, framebuffer, cubemap, dir.target, projectionMatrix, viewMatrix, size); }); return cubemap;}立方体贴图采样原理立方体贴图使用 3D 方向向量 (x, y, z) 进行采样,选择哪个面取决于哪个分量的绝对值最大:如果 |x| 最大: x > 0: 使用 +X 面,坐标 ( -z/x, -y/x ) x < 0: 使用 -X 面,坐标 ( z/x, -y/x )如果 |y| 最大: y > 0: 使用 +Y 面,坐标 ( x/y, z/y ) y < 0: 使用 -Y 面,坐标 ( x/y, -z/y )如果 |z| 最大: z > 0: 使用 +Z 面,坐标 ( x/z, -y/z ) z < 0: 使用 -Z 面,坐标 ( -x/z, -y/z )性能优化建议预过滤环境贴图:为不同粗糙度预计算模糊的立方体贴图使用 mipmap 级别存储不同粗糙度的反射立方体贴图压缩:使用压缩纹理格式减少内存占用DXT、ETC、PVRTC 等格式动态环境贴图优化:降低分辨率(如 128x128)减少更新频率(如每 10 帧更新一次)只更新可见物体的反射LOD 系统:远处物体使用低分辨率立方体贴图近处物体使用高分辨率立方体贴图常见问题接缝问题立方体贴图面与面之间可能出现接缝。解决方案:// 使用 CLAMP_TO_EDGE 环绕模式gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE);图片方向问题不同面的图片可能需要翻转。解决方案:使用图像编辑软件预先调整或在着色器中翻转纹理坐标// 翻转 Y 坐标vec2 flippedCoord = vec2(texCoord.x, 1.0 - texCoord.y);
阅读 0·3月7日 12:04

WebGL 中的缓冲区(Buffer)是什么?如何使用 VBO 和 VAO?

WebGL 缓冲区概述缓冲区(Buffer)是 GPU 内存中的一块区域,用于存储顶点数据、索引数据等图形渲染所需的信息。使用缓冲区可以高效地将数据从 CPU 传输到 GPU。缓冲区类型1. 顶点缓冲区对象(VBO - Vertex Buffer Object)存储顶点属性数据,如位置、颜色、法线、纹理坐标等。2. 索引缓冲区对象(IBO/EBO - Index/Element Buffer Object)存储顶点索引,用于定义图元的连接方式,减少重复顶点数据。3. 顶点数组对象(VAO - Vertex Array Object)存储顶点属性配置的状态,简化绘制调用前的设置。VBO(顶点缓冲区对象)详解创建和使用 VBO// 1. 创建缓冲区const vbo = gl.createBuffer();// 2. 绑定缓冲区(指定当前操作的缓冲区类型)gl.bindBuffer(gl.ARRAY_BUFFER, vbo);// 3. 上传数据到 GPUconst vertices = new Float32Array([ // 位置(x, y, z) 颜色(r, g, b) -0.5, -0.5, 0.0, 1.0, 0.0, 0.0, // 顶点1 0.5, -0.5, 0.0, 0.0, 1.0, 0.0, // 顶点2 0.0, 0.5, 0.0, 0.0, 0.0, 1.0 // 顶点3]);gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);// 4. 配置顶点属性指针gl.vertexAttribPointer( 0, // 属性位置(对应着色器中的 layout(location=0)) 3, // 每个属性的分量数(这里是3:x, y, z) gl.FLOAT, // 数据类型 false, // 是否归一化 6 * 4, // 步长(每个顶点的字节数:6个float * 4字节) 0 // 偏移量);gl.enableVertexAttribArray(0); // 启用位置属性// 配置颜色属性gl.vertexAttribPointer( 1, // 属性位置 3, // 分量数(r, g, b) gl.FLOAT, false, 6 * 4, // 步长 3 * 4 // 偏移量(跳过位置数据));gl.enableVertexAttribArray(1); // 启用颜色属性缓冲区使用模式// gl.STATIC_DRAW:数据不经常改变,多次绘制 gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);// gl.DYNAMIC_DRAW:数据经常改变,多次绘制gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);// gl.STREAM_DRAW:数据每次绘制都改变gl.bufferData(gl.ARRAY_BUFFER, data, gl.STREAM_DRAW);IBO/EBO(索引缓冲区对象)详解创建和使用 IBO// 顶点数据(4个顶点定义一个四边形)const vertices = new Float32Array([ -0.5, 0.5, 0.0, // 左上 (0) -0.5, -0.5, 0.0, // 左下 (1) 0.5, -0.5, 0.0, // 右下 (2) 0.5, 0.5, 0.0 // 右上 (3)]);// 索引数据(2个三角形 = 6个索引)const indices = new Uint16Array([ 0, 1, 2, // 第一个三角形 0, 2, 3 // 第二个三角形]);// 创建并绑定 VBOgl.bindBuffer(gl.ARRAY_BUFFER, vbo);gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);// 创建并绑定 IBOconst ibo = gl.createBuffer();gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);// 绘制(使用索引绘制)gl.drawElements( gl.TRIANGLES, // 绘制模式 6, // 索引数量 gl.UNSIGNED_SHORT, // 索引数据类型 0 // 偏移量);VAO(顶点数组对象)详解VAO 存储了所有顶点属性的配置状态,包括:启用的顶点属性每个属性的配置(大小、类型、步长、偏移)绑定的 VBOWebGL 2.0 / WebGL 1.0 + OESvertexarray_object 扩展// 创建 VAOconst vao = gl.createVertexArray(); // WebGL 2.0// const vao = ext.createVertexArrayOES(); // 使用扩展// 绑定 VAO(后续配置将存储在 VAO 中)gl.bindVertexArray(vao);// 配置顶点属性(这些配置会被 VAO 记录)gl.bindBuffer(gl.ARRAY_BUFFER, positionVBO);gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);gl.enableVertexAttribArray(0);gl.bindBuffer(gl.ARRAY_BUFFER, colorVBO);gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);gl.enableVertexAttribArray(1);gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);// 解绑 VAOgl.bindVertexArray(null);// 绘制时只需绑定 VAOgl.bindVertexArray(vao);gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);完整示例代码class Mesh { constructor(gl) { this.gl = gl; this.vertexCount = 0; this.indexCount = 0; // WebGL 2.0 创建 VAO this.vao = gl.createVertexArray(); this.vbo = gl.createBuffer(); this.ibo = gl.createBuffer(); } setVertices(vertices, attributes) { const gl = this.gl; gl.bindVertexArray(this.vao); // 上传顶点数据 gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo); gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); // 配置属性 let offset = 0; const stride = attributes.reduce((sum, attr) => sum + attr.size, 0) * 4; attributes.forEach((attr, index) => { gl.vertexAttribPointer( index, attr.size, gl.FLOAT, false, stride, offset ); gl.enableVertexAttribArray(index); offset += attr.size * 4; }); this.vertexCount = vertices.length / (stride / 4); } setIndices(indices) { const gl = this.gl; gl.bindVertexArray(this.vao); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ibo); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW); this.indexCount = indices.length; } draw() { const gl = this.gl; gl.bindVertexArray(this.vao); if (this.indexCount > 0) { gl.drawElements(gl.TRIANGLES, this.indexCount, gl.UNSIGNED_SHORT, 0); } else { gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount); } }}// 使用示例const mesh = new Mesh(gl);mesh.setVertices( new Float32Array([/* 顶点数据 */]), [ { size: 3 }, // 位置 { size: 3 }, // 颜色 { size: 2 } // 纹理坐标 ]);mesh.setIndices(new Uint16Array([/* 索引数据 */]));mesh.draw();性能优化建议减少状态切换:使用 VAO 减少绘制前的配置开销合并缓冲区:将多个小网格合并到一个大缓冲区使用索引绘制:减少顶点数据重复选择合适的绘制模式:STATIC_DRAW:静态几何体DYNAMIC_DRAW:频繁更新的数据STREAM_DRAW:每帧都更新的数据批量绘制:使用实例化渲染(Instanced Rendering)绘制多个相同对象
阅读 0·3月7日 12:04

WebGL 中的雾效(Fog)是如何实现的?

WebGL 雾效概述雾效(Fog)是一种模拟大气散射效果的渲染技术,使远处的物体逐渐融入到背景颜色中。雾效不仅能增加场景的真实感,还能隐藏远处的裁剪边界,优化性能。雾效的基本原理雾效的核心思想是根据物体与相机的距离,在物体颜色和雾颜色之间进行线性或指数插值:最终颜色 = 物体颜色 × (1 - 雾因子) + 雾颜色 × 雾因子雾的类型1. 线性雾(Linear Fog)雾的浓度随距离线性增加。公式:fogFactor = (end - distance) / (end - start)start:雾开始距离end:雾完全覆盖距离distance:片段到相机的距离// 顶点着色器varying float v_fogDepth;void main() { vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0); vec4 viewPos = u_viewMatrix * worldPos; // 计算视图空间深度(正值) v_fogDepth = -viewPos.z; gl_Position = u_projectionMatrix * viewPos;}// 片段着色器uniform vec3 u_fogColor;uniform float u_fogStart;uniform float u_fogEnd;varying float v_fogDepth;void main() { vec4 objectColor = texture2D(u_texture, v_texCoord); // 计算线性雾因子 float fogFactor = (u_fogEnd - v_fogDepth) / (u_fogEnd - u_fogStart); fogFactor = clamp(fogFactor, 0.0, 1.0); // 混合物体颜色和雾颜色 vec3 finalColor = mix(u_fogColor, objectColor.rgb, fogFactor); gl_FragColor = vec4(finalColor, objectColor.a);}2. 指数雾(Exponential Fog)雾的浓度随距离指数增加,效果更自然。公式:fogFactor = exp(-density × distance)uniform vec3 u_fogColor;uniform float u_fogDensity;varying float v_fogDepth;void main() { vec4 objectColor = texture2D(u_texture, v_texCoord); // 计算指数雾因子 float fogFactor = exp(-u_fogDensity * v_fogDepth); fogFactor = clamp(fogFactor, 0.0, 1.0); vec3 finalColor = mix(u_fogColor, objectColor.rgb, fogFactor); gl_FragColor = vec4(finalColor, objectColor.a);}3. 指数平方雾(Exp2 Fog)雾的浓度随距离平方指数增加,效果更加柔和。公式:fogFactor = exp(-(density × distance)²)void main() { vec4 objectColor = texture2D(u_texture, v_texCoord); // 计算指数平方雾因子 float fogFactor = exp(-pow(u_fogDensity * v_fogDepth, 2.0)); fogFactor = clamp(fogFactor, 0.0, 1.0); vec3 finalColor = mix(u_fogColor, objectColor.rgb, fogFactor); gl_FragColor = vec4(finalColor, objectColor.a);}完整的雾效实现顶点着色器attribute vec3 a_position;attribute vec2 a_texCoord;uniform mat4 u_modelMatrix;uniform mat4 u_viewMatrix;uniform mat4 u_projectionMatrix;varying vec2 v_texCoord;varying float v_fogDepth;void main() { vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0); vec4 viewPos = u_viewMatrix * worldPos; v_texCoord = a_texCoord; v_fogDepth = length(viewPos.xyz); // 使用实际距离而非仅 Z 值 gl_Position = u_projectionMatrix * viewPos;}片段着色器precision mediump float;uniform sampler2D u_texture;uniform vec3 u_fogColor;uniform float u_fogDensity;uniform int u_fogType; // 0: 线性, 1: 指数, 2: 指数平方uniform float u_fogStart;uniform float u_fogEnd;varying vec2 v_texCoord;varying float v_fogDepth;float calculateFogFactor() { float fogFactor = 0.0; if (u_fogType == 0) { // 线性雾 fogFactor = (u_fogEnd - v_fogDepth) / (u_fogEnd - u_fogStart); } else if (u_fogType == 1) { // 指数雾 fogFactor = exp(-u_fogDensity * v_fogDepth); } else if (u_fogType == 2) { // 指数平方雾 fogFactor = exp(-pow(u_fogDensity * v_fogDepth, 2.0)); } return clamp(fogFactor, 0.0, 1.0);}void main() { vec4 objectColor = texture2D(u_texture, v_texCoord); float fogFactor = calculateFogFactor(); // mix(fogColor, objectColor, fogFactor) vec3 finalColor = mix(u_fogColor, objectColor.rgb, fogFactor); gl_FragColor = vec4(finalColor, objectColor.a);}JavaScript 控制class Fog { constructor(gl, program) { this.gl = gl; this.program = program; // 获取 uniform 位置 this.fogColorLoc = gl.getUniformLocation(program, 'u_fogColor'); this.fogDensityLoc = gl.getUniformLocation(program, 'u_fogDensity'); this.fogTypeLoc = gl.getUniformLocation(program, 'u_fogType'); this.fogStartLoc = gl.getUniformLocation(program, 'u_fogStart'); this.fogEndLoc = gl.getUniformLocation(program, 'u_fogEnd'); // 默认设置 this.color = [0.7, 0.8, 0.9]; // 淡蓝色雾 this.density = 0.02; this.type = 2; // 指数平方雾 this.start = 10.0; this.end = 50.0; } apply() { const gl = this.gl; gl.uniform3fv(this.fogColorLoc, this.color); gl.uniform1f(this.fogDensityLoc, this.density); gl.uniform1i(this.fogTypeLoc, this.type); gl.uniform1f(this.fogStartLoc, this.start); gl.uniform1f(this.fogEndLoc, this.end); } // 设置雾的类型 setLinear(start, end) { this.type = 0; this.start = start; this.end = end; } setExponential(density) { this.type = 1; this.density = density; } setExponentialSquared(density) { this.type = 2; this.density = density; }}// 使用示例const fog = new Fog(gl, program);fog.setExponentialSquared(0.015);fog.apply();高度雾(Height Fog)模拟根据高度变化的雾效,如山谷中的雾。// 顶点着色器varying vec3 v_worldPos;void main() { vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0); v_worldPos = worldPos.xyz; gl_Position = u_projectionMatrix * u_viewMatrix * worldPos;}// 片段着色器uniform float u_fogHeight;uniform float u_fogHeightFalloff;varying vec3 v_worldPos;float calculateHeightFog() { // 基于高度的雾密度 float heightFactor = (u_fogHeight - v_worldPos.y) * u_fogHeightFalloff; heightFactor = clamp(heightFactor, 0.0, 1.0); // 结合距离雾 float distanceFactor = exp(-u_fogDensity * length(v_worldPos - u_cameraPos)); return distanceFactor * (1.0 - heightFactor);}雾效与光照结合void main() { // 计算光照 vec3 ambient = ...; vec3 diffuse = ...; vec3 specular = ...; vec3 lighting = ambient + diffuse + specular; vec4 texColor = texture2D(u_texture, v_texCoord); vec3 objectColor = lighting * texColor.rgb; // 应用雾效 float fogFactor = calculateFogFactor(); vec3 finalColor = mix(u_fogColor, objectColor, fogFactor); gl_FragColor = vec4(finalColor, texColor.a);}雾效的性能优化1. 顶点级雾效计算当顶点数较少时,可以在顶点着色器中计算雾因子:// 顶点着色器varying float v_fogFactor;void main() { // ... 计算位置 float fogDepth = length(viewPos.xyz); v_fogFactor = exp(-u_fogDensity * fogDepth); v_fogFactor = clamp(v_fogFactor, 0.0, 1.0);}// 片段着色器varying float v_fogFactor;void main() { vec3 finalColor = mix(u_fogColor, objectColor, v_fogFactor);}2. 使用深度纹理在后处理阶段应用雾效:// 1. 渲染场景到颜色纹理和深度纹理renderSceneToTextures();// 2. 后处理阶段应用雾效applyFogPostProcess();// 后处理雾效着色器uniform sampler2D u_colorTexture;uniform sampler2D u_depthTexture;void main() { vec3 color = texture2D(u_colorTexture, v_texCoord).rgb; float depth = texture2D(u_depthTexture, v_texCoord).r; // 将深度转换为世界空间距离 float linearDepth = linearizeDepth(depth); // 计算雾因子 float fogFactor = exp(-u_fogDensity * linearDepth); vec3 finalColor = mix(u_fogColor, color, fogFactor); gl_FragColor = vec4(finalColor, 1.0);}不同类型的雾效对比| 雾类型 | 公式 | 特点 ||--------|------|------|| 线性雾 | (end - d) / (end - start) | 简单,雾边界明显 || 指数雾 | exp(-density × d) | 自然,适合大多数场景 || 指数平方雾 | exp(-(density × d)²) | 更柔和,雾边界不明显 || 高度雾 | 结合 Y 轴 | 适合山谷、水面等场景 |实际应用建议选择合适的雾类型:大多数场景使用指数或指数平方雾需要精确控制雾边界时使用线性雾调整雾的颜色:通常与天空盒或背景色一致可以随时间变化模拟昼夜效果性能考虑:移动端建议使用顶点级雾效复杂场景可以使用后处理雾效与其他效果结合:雾效可以与体积光、大气散射结合注意雾效对透明物体的影响
阅读 0·3月7日 12:04

WebGL 中的矩阵变换有哪些?MVP 矩阵是什么?

WebGL 中的矩阵变换在 3D 图形渲染中,矩阵变换用于将顶点从一个坐标空间转换到另一个坐标空间。WebGL 使用 4×4 矩阵进行各种变换操作。基本变换矩阵1. 平移矩阵(Translation Matrix)将物体沿 X、Y、Z 轴移动| 1 0 0 tx || 0 1 0 ty || 0 0 1 tz || 0 0 0 1 |function createTranslationMatrix(tx, ty, tz) { return new Float32Array([ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, tx, ty, tz, 1 ]);}2. 缩放矩阵(Scale Matrix)沿各轴缩放物体大小| sx 0 0 0 || 0 sy 0 0 || 0 0 sz 0 || 0 0 0 1 |3. 旋转矩阵(Rotation Matrix)绕 X 轴旋转| 1 0 0 0 || 0 cosθ -sinθ 0 || 0 sinθ cosθ 0 || 0 0 0 1 |绕 Y 轴旋转| cosθ 0 sinθ 0 || 0 1 0 0 || -sinθ 0 cosθ 0 || 0 0 0 1 |绕 Z 轴旋转| cosθ -sinθ 0 0 || sinθ cosθ 0 0 || 0 0 1 0 || 0 0 0 1 |MVP 矩阵详解MVP 矩阵是三个矩阵的乘积,用于将顶点从模型空间转换到裁剪空间:MVP = P × V × MM - 模型矩阵(Model Matrix)作用:将顶点从模型空间(局部空间)转换到世界空间// 模型矩阵 = 平移 × 旋转 × 缩放const modelMatrix = mat4.create();mat4.translate(modelMatrix, modelMatrix, [x, y, z]);mat4.rotateX(modelMatrix, modelMatrix, angleX);mat4.rotateY(modelMatrix, modelMatrix, angleY);mat4.scale(modelMatrix, modelMatrix, [sx, sy, sz]);应用场景:物体在世界中的位置物体的旋转角度物体的大小缩放V - 视图矩阵(View Matrix)作用:将顶点从世界空间转换到相机空间(观察空间)// 使用 lookAt 函数创建视图矩阵const viewMatrix = mat4.create();mat4.lookAt( viewMatrix, [0, 0, 5], // 相机位置(眼睛) [0, 0, 0], // 观察目标点 [0, 1, 0] // 上方向向量);视图矩阵的本质:将世界坐标系原点移动到相机位置旋转坐标系使相机朝向 -Z 方向相机位于原点,看向 -Z 轴方向P - 投影矩阵(Projection Matrix)作用:将顶点从相机空间转换到裁剪空间透视投影(Perspective Projection)模拟人眼视觉效果,近大远小const projectionMatrix = mat4.create();mat4.perspective( projectionMatrix, Math.PI / 4, // 视野角度(FOV) canvas.width / canvas.height, // 宽高比 0.1, // 近平面 100.0 // 远平面);正交投影(Orthographic Projection)保持物体大小不变,常用于 2D 游戏或 CAD 软件mat4.ortho( projectionMatrix, -2, 2, // 左右 -2, 2, // 下上 0.1, 100 // 近远);坐标空间转换流程模型空间(局部空间) ↓ [模型矩阵 M]世界空间 ↓ [视图矩阵 V]相机空间(观察空间) ↓ [投影矩阵 P]裁剪空间 ↓ [透视除法]标准化设备坐标(NDC) ↓ [视口变换]屏幕空间顶点着色器中的 MVP 应用// 顶点着色器attribute vec3 a_position;attribute vec3 a_color;uniform mat4 u_modelMatrix;uniform mat4 u_viewMatrix;uniform mat4 u_projectionMatrix;varying vec3 v_color;void main() { // 方法1:分别应用三个矩阵 // vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0); // vec4 viewPos = u_viewMatrix * worldPos; // gl_Position = u_projectionMatrix * viewPos; // 方法2:使用预计算的 MVP 矩阵(推荐) mat4 mvp = u_projectionMatrix * u_viewMatrix * u_modelMatrix; gl_Position = mvp * vec4(a_position, 1.0); v_color = a_color;}JavaScript 中的矩阵计算// 使用 gl-matrix 库import { mat4, vec3 } from 'gl-matrix';class Camera { constructor() { this.projectionMatrix = mat4.create(); this.viewMatrix = mat4.create(); this.mvpMatrix = mat4.create(); } setPerspective(fov, aspect, near, far) { mat4.perspective(this.projectionMatrix, fov, aspect, near, far); } lookAt(eye, center, up) { mat4.lookAt(this.viewMatrix, eye, center, up); } getMVPMatrix(modelMatrix) { // MVP = P × V × M mat4.multiply(this.mvpMatrix, this.viewMatrix, modelMatrix); mat4.multiply(this.mvpMatrix, this.projectionMatrix, this.mvpMatrix); return this.mvpMatrix; }}// 使用示例const camera = new Camera();camera.setPerspective(Math.PI / 4, 16/9, 0.1, 100);camera.lookAt([0, 0, 5], [0, 0, 0], [0, 1, 0]);const modelMatrix = mat4.create();mat4.translate(modelMatrix, modelMatrix, [1, 0, 0]);const mvp = camera.getMVPMatrix(modelMatrix);gl.uniformMatrix4fv(mvpLocation, false, mvp);常见问题与注意事项1. 矩阵乘法顺序矩阵乘法不满足交换律,顺序非常重要:正确:MVP = P × V × M,应用于顶点:MVP × 顶点先应用的变换在乘法链的右侧2. 行主序 vs 列主序WebGL 使用列主序存储矩阵gl.uniformMatrix4fv 的第三个参数 transpose 必须为 false3. 齐次坐标使用 4D 向量 (x, y, z, w):顶点位置:w = 1方向向量:w = 0(不受平移影响)4. 性能优化在 CPU 端预计算 MVP 矩阵,而不是在着色器中分别相乘使用 uniform 传递预计算的矩阵
阅读 0·3月6日 21:58

WebGL 中的光照模型有哪些?如何实现 Phong 光照模型?

WebGL 光照模型概述光照模型用于模拟光线与物体表面的交互,是 3D 渲染中实现真实感的关键技术。WebGL 中常用的光照模型包括:环境光(Ambient)、漫反射(Diffuse)和镜面反射(Specular)。基本光照模型1. 环境光(Ambient Lighting)模拟场景中无处不在的间接光照,不考虑光源位置和方向。vec3 ambient = ambientStrength * lightColor;2. 漫反射(Diffuse Lighting)模拟光线照射到粗糙表面后向各个方向均匀反射的效果。遵循朗伯余弦定律。// 计算光线方向与法线的夹角float diff = max(dot(normal, lightDir), 0.0);vec3 diffuse = diff * lightColor;3. 镜面反射(Specular Lighting)模拟光线在光滑表面的定向反射,产生高光效果。// Phong 模型vec3 reflectDir = reflect(-lightDir, normal);float spec = pow(max(dot(viewDir, reflectDir), 0.0), shininess);vec3 specular = specularStrength * spec * lightColor;// Blinn-Phong 模型(更高效)vec3 halfwayDir = normalize(lightDir + viewDir);float spec = pow(max(dot(normal, halfwayDir), 0.0), shininess);vec3 specular = specularStrength * spec * lightColor;Phong 光照模型详解Phong 光照模型是三种光照成分的组合:最终颜色 = 环境光 + 漫反射 + 镜面反射顶点着色器attribute vec3 a_position;attribute vec3 a_normal;attribute vec2 a_texCoord;uniform mat4 u_modelMatrix;uniform mat4 u_viewMatrix;uniform mat4 u_projectionMatrix;uniform mat3 u_normalMatrix; // 法线矩阵(用于正确变换法线)uniform vec3 u_lightPosition;uniform vec3 u_cameraPosition;varying vec3 v_normal;varying vec3 v_lightDir;varying vec3 v_viewDir;varying vec2 v_texCoord;void main() { vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0); gl_Position = u_projectionMatrix * u_viewMatrix * worldPos; // 变换法线到世界空间 v_normal = normalize(u_normalMatrix * a_normal); // 计算光线方向(从片段指向光源) v_lightDir = normalize(u_lightPosition - worldPos.xyz); // 计算视线方向(从片段指向相机) v_viewDir = normalize(u_cameraPosition - worldPos.xyz); v_texCoord = a_texCoord;}片段着色器(Phong 模型)precision mediump float;varying vec3 v_normal;varying vec3 v_lightDir;varying vec3 v_viewDir;varying vec2 v_texCoord;uniform vec3 u_lightColor;uniform vec3 u_ambientColor;uniform sampler2D u_diffuseMap;uniform float u_shininess;uniform float u_ambientStrength;uniform float u_specularStrength;void main() { vec3 normal = normalize(v_normal); vec3 lightDir = normalize(v_lightDir); vec3 viewDir = normalize(v_viewDir); // 环境光 vec3 ambient = u_ambientStrength * u_ambientColor; // 漫反射 float diff = max(dot(normal, lightDir), 0.0); vec3 diffuse = diff * u_lightColor; // 镜面反射(Phong) vec3 reflectDir = reflect(-lightDir, normal); float spec = pow(max(dot(viewDir, reflectDir), 0.0), u_shininess); vec3 specular = u_specularStrength * spec * u_lightColor; // 采样纹理 vec4 texColor = texture2D(u_diffuseMap, v_texCoord); // 组合光照 vec3 lighting = ambient + diffuse + specular; vec3 result = lighting * texColor.rgb; gl_FragColor = vec4(result, texColor.a);}片段着色器(Blinn-Phong 模型)Blinn-Phong 是 Phong 的改进版本,使用半角向量代替反射向量,计算更高效。void main() { vec3 normal = normalize(v_normal); vec3 lightDir = normalize(v_lightDir); vec3 viewDir = normalize(v_viewDir); // 环境光 vec3 ambient = u_ambientStrength * u_ambientColor; // 漫反射 float diff = max(dot(normal, lightDir), 0.0); vec3 diffuse = diff * u_lightColor; // 镜面反射(Blinn-Phong) vec3 halfwayDir = normalize(lightDir + viewDir); float spec = pow(max(dot(normal, halfwayDir), 0.0), u_shininess); vec3 specular = u_specularStrength * spec * u_lightColor; // 组合光照 vec3 lighting = ambient + diffuse + specular; vec3 result = lighting * texture2D(u_diffuseMap, v_texCoord).rgb; gl_FragColor = vec4(result, 1.0);}法线矩阵的计算// 法线矩阵是模型矩阵的逆转置矩阵的左上 3x3 部分function createNormalMatrix(modelMatrix) { const normalMatrix = mat4.create(); mat4.invert(normalMatrix, modelMatrix); mat4.transpose(normalMatrix, normalMatrix); // 提取 3x3 部分 return new Float32Array([ normalMatrix[0], normalMatrix[1], normalMatrix[2], normalMatrix[4], normalMatrix[5], normalMatrix[6], normalMatrix[8], normalMatrix[9], normalMatrix[10] ]);}// 或使用 gl-matrix 的 mat3const normalMatrix = mat3.create();mat3.fromMat4(normalMatrix, modelMatrix);mat3.invert(normalMatrix, normalMatrix);mat3.transpose(normalMatrix, normalMatrix);多光源处理#define MAX_LIGHTS 4struct Light { vec3 position; vec3 color; float intensity;};uniform Light u_lights[MAX_LIGHTS];uniform int u_numLights;vec3 calculateLighting(vec3 normal, vec3 viewDir, vec3 fragPos) { vec3 result = u_ambientColor * u_ambientStrength; for (int i = 0; i < MAX_LIGHTS; i++) { if (i >= u_numLights) break; Light light = u_lights[i]; vec3 lightDir = normalize(light.position - fragPos); // 漫反射 float diff = max(dot(normal, lightDir), 0.0); vec3 diffuse = diff * light.color * light.intensity; // 镜面反射 vec3 halfwayDir = normalize(lightDir + viewDir); float spec = pow(max(dot(normal, halfwayDir), 0.0), u_shininess); vec3 specular = u_specularStrength * spec * light.color * light.intensity; // 衰减 float distance = length(light.position - fragPos); float attenuation = 1.0 / (1.0 + 0.09 * distance + 0.032 * distance * distance); result += (diffuse + specular) * attenuation; } return result;}不同光源类型方向光(Directional Light)// 方向光只有方向,没有位置uniform vec3 u_lightDirection; // 光线方向vec3 lightDir = normalize(-u_lightDirection); // 指向光源float diff = max(dot(normal, lightDir), 0.0);点光源(Point Light)// 点光源有位置,向四面八方发射uniform vec3 u_lightPosition;uniform float u_constant;uniform float u_linear;uniform float u_quadratic;vec3 lightDir = normalize(u_lightPosition - fragPos);float distance = length(u_lightPosition - fragPos);float attenuation = 1.0 / (u_constant + u_linear * distance + u_quadratic * distance * distance);聚光灯(Spot Light)// 聚光灯有位置、方向和角度限制uniform vec3 u_lightPosition;uniform vec3 u_lightDirection;uniform float u_cutOff; // 内切角余弦uniform float u_outerCutOff; // 外切角余弦vec3 lightDir = normalize(u_lightPosition - fragPos);float theta = dot(lightDir, normalize(-u_lightDirection));float epsilon = u_cutOff - u_outerCutOff;float intensity = clamp((theta - u_outerCutOff) / epsilon, 0.0, 1.0);材质属性struct Material { vec3 ambient; vec3 diffuse; vec3 specular; float shininess;};uniform Material u_material;void main() { vec3 ambient = u_light.ambient * u_material.ambient; vec3 diffuse = u_light.diffuse * (diff * u_material.diffuse); vec3 specular = u_light.specular * (spec * u_material.specular);}Gouraud 着色 vs Phong 着色| 特性 | Gouraud 着色 | Phong 着色 ||------|-------------|------------|| 计算位置 | 顶点着色器 | 片段着色器 || 质量 | 较低(插值) | 较高(逐像素) || 性能 | 更快 | 较慢 || 高光 | 可能不准确 | 准确 |// Gouraud 着色(顶点着色器中计算光照)varying vec3 v_lighting;void main() { // 在顶点级别计算光照 vec3 ambient = ...; vec3 diffuse = ...; vec3 specular = ...; v_lighting = ambient + diffuse + specular;}// 片段着色器void main() { gl_FragColor = vec4(v_lighting * texColor, 1.0);}性能优化建议顶点着色器 vs 片段着色器:顶点数 < 片段数时,在顶点着色器计算需要高质量光照时,在片段着色器计算使用 Blinn-Phong:比 Phong 更高效(避免计算 reflect)视觉效果相似限制光源数量:移动端建议 1-2 个光源桌面端建议 4-8 个光源使用延迟渲染:大量光源时使用避免对每个光源遍历所有片段
阅读 0·3月6日 21:57

WebGL 中的后期处理(Post-processing)是如何实现的?

WebGL 后期处理概述后期处理(Post-processing)是在场景渲染完成后,对渲染结果进行图像处理的技术。通过后期处理可以实现各种视觉效果,如模糊、辉光、色调映射、抗锯齿等。后期处理的基本原理后期处理的核心流程:将场景渲染到纹理(离屏渲染)对纹理进行各种图像处理将处理后的结果渲染到屏幕场景渲染 → 颜色纹理 → 后期处理着色器 → 屏幕 ↓ 深度纹理(可选)基本后期处理框架创建后期处理所需的资源class PostProcess { constructor(gl, width, height) { this.gl = gl; this.width = width; this.height = height; // 创建帧缓冲区 this.framebuffer = gl.createFramebuffer(); // 创建颜色纹理 this.colorTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, this.colorTexture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // 绑定到帧缓冲区 gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.colorTexture, 0); // 创建深度渲染缓冲区 this.depthBuffer = gl.createRenderbuffer(); gl.bindRenderbuffer(gl.RENDERBUFFER, this.depthBuffer); gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, this.depthBuffer); gl.bindFramebuffer(gl.FRAMEBUFFER, null); // 创建全屏四边形 this.quad = this.createFullscreenQuad(); } createFullscreenQuad() { // 返回 VAO 或顶点数据 const vertices = new Float32Array([ // 位置 // 纹理坐标 -1, 1, 0, 1, -1, -1, 0, 0, 1, 1, 1, 1, 1, -1, 1, 0 ]); // 创建 VBO 等... return { vertices, vbo: this.gl.createBuffer() }; } beginScene() { this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.framebuffer); this.gl.viewport(0, 0, this.width, this.height); this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT); } endScene() { this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null); } apply(effectProgram) { this.gl.useProgram(effectProgram); this.gl.activeTexture(this.gl.TEXTURE0); this.gl.bindTexture(this.gl.TEXTURE_2D, this.colorTexture); this.gl.uniform1i(this.gl.getUniformLocation(effectProgram, 'u_texture'), 0); // 绘制全屏四边形 this.drawQuad(); } drawQuad() { // 绑定 VAO 并绘制 this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4); }}后期处理顶点着色器attribute vec2 a_position;attribute vec2 a_texCoord;varying vec2 v_texCoord;void main() { gl_Position = vec4(a_position, 0.0, 1.0); v_texCoord = a_texCoord;}常用后期处理效果1. 灰度效果(Grayscale)precision mediump float;varying vec2 v_texCoord;uniform sampler2D u_texture;void main() { vec4 color = texture2D(u_texture, v_texCoord); // 加权灰度转换 float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114)); gl_FragColor = vec4(vec3(gray), color.a);}2. 模糊效果(Blur)高斯模糊precision mediump float;varying vec2 v_texCoord;uniform sampler2D u_texture;uniform vec2 u_texelSize; // 1.0 / textureSizeuniform vec2 u_direction; // 模糊方向 (1,0) 或 (0,1)void main() { vec4 color = vec4(0.0); // 5x5 高斯核 float weights[5]; weights[0] = 0.227027; weights[1] = 0.1945946; weights[2] = 0.1216216; weights[3] = 0.054054; weights[4] = 0.016216; // 中心像素 color += texture2D(u_texture, v_texCoord) * weights[0]; // 两侧像素 for (int i = 1; i < 5; i++) { vec2 offset = u_direction * u_texelSize * float(i); color += texture2D(u_texture, v_texCoord + offset) * weights[i]; color += texture2D(u_texture, v_texCoord - offset) * weights[i]; } gl_FragColor = color;}双通道模糊(优化性能)// 先水平模糊horizontalBlurProgram.setUniform('u_direction', [1, 0]);postProcess.apply(horizontalBlurProgram);// 再垂直模糊verticalBlurProgram.setUniform('u_direction', [0, 1]);postProcess.apply(verticalBlurProgram);3. 边缘检测(Edge Detection)precision mediump float;varying vec2 v_texCoord;uniform sampler2D u_texture;uniform vec2 u_texelSize;void main() { // Sobel 算子 float kernelX[9]; kernelX[0] = -1.0; kernelX[1] = 0.0; kernelX[2] = 1.0; kernelX[3] = -2.0; kernelX[4] = 0.0; kernelX[5] = 2.0; kernelX[6] = -1.0; kernelX[7] = 0.0; kernelX[8] = 1.0; float kernelY[9]; kernelY[0] = -1.0; kernelY[1] = -2.0; kernelY[2] = -1.0; kernelY[3] = 0.0; kernelY[4] = 0.0; kernelY[5] = 0.0; kernelY[6] = 1.0; kernelY[7] = 2.0; kernelY[8] = 1.0; vec2 offsets[9]; offsets[0] = vec2(-1, -1); offsets[1] = vec2(0, -1); offsets[2] = vec2(1, -1); offsets[3] = vec2(-1, 0); offsets[4] = vec2(0, 0); offsets[5] = vec2(1, 0); offsets[6] = vec2(-1, 1); offsets[7] = vec2(0, 1); offsets[8] = vec2(1, 1); float edgeX = 0.0; float edgeY = 0.0; for (int i = 0; i < 9; i++) { vec2 coord = v_texCoord + offsets[i] * u_texelSize; float gray = dot(texture2D(u_texture, coord).rgb, vec3(0.299, 0.587, 0.114)); edgeX += gray * kernelX[i]; edgeY += gray * kernelY[i]; } float edge = sqrt(edgeX * edgeX + edgeY * edgeY); gl_FragColor = vec4(vec3(edge), 1.0);}4. 辉光效果(Bloom)// 提取高亮部分precision mediump float;varying vec2 v_texCoord;uniform sampler2D u_texture;uniform float u_threshold;void main() { vec4 color = texture2D(u_texture, v_texCoord); float brightness = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722)); if (brightness > u_threshold) { gl_FragColor = color; } else { gl_FragColor = vec4(0.0); }}// 合成辉光precision mediump float;varying vec2 v_texCoord;uniform sampler2D u_sceneTexture;uniform sampler2D u_bloomTexture;uniform float u_bloomIntensity;void main() { vec4 sceneColor = texture2D(u_sceneTexture, v_texCoord); vec4 bloomColor = texture2D(u_bloomTexture, v_texCoord); gl_FragColor = sceneColor + bloomColor * u_bloomIntensity;}5. 色调映射(Tone Mapping)precision mediump float;varying vec2 v_texCoord;uniform sampler2D u_texture;uniform float u_exposure;// Reinhard 色调映射vec3 reinhardToneMapping(vec3 color) { return color / (color + vec3(1.0));}// ACES 色调映射vec3 acesToneMapping(vec3 color) { const float a = 2.51; const float b = 0.03; const float c = 2.43; const float d = 0.59; const float e = 0.14; return clamp((color * (a * color + b)) / (color * (c * color + d) + e), 0.0, 1.0);}void main() { vec4 color = texture2D(u_texture, v_texCoord); // 曝光调整 vec3 mapped = vec3(1.0) - exp(-color.rgb * u_exposure); // Gamma 校正 mapped = pow(mapped, vec3(1.0 / 2.2)); gl_FragColor = vec4(mapped, color.a);}6. 色彩调整precision mediump float;varying vec2 v_texCoord;uniform sampler2D u_texture;uniform float u_brightness;uniform float u_contrast;uniform float u_saturation;void main() { vec4 color = texture2D(u_texture, v_texCoord); // 亮度 color.rgb += u_brightness; // 对比度 color.rgb = (color.rgb - 0.5) * u_contrast + 0.5; // 饱和度 float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114)); color.rgb = mix(vec3(gray), color.rgb, u_saturation); gl_FragColor = color;}7. 屏幕空间环境光遮蔽(SSAO)precision mediump float;varying vec2 v_texCoord;uniform sampler2D u_colorTexture;uniform sampler2D u_depthTexture;uniform sampler2D u_normalTexture;uniform mat4 u_projectionMatrix;uniform mat4 u_viewMatrix;uniform vec2 u_texelSize;uniform vec3 u_samples[64]; // 采样核vec3 getViewPosition(vec2 texCoord) { float depth = texture2D(u_depthTexture, texCoord).r; vec4 clipPos = vec4(texCoord * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0); vec4 viewPos = inverse(u_projectionMatrix) * clipPos; return viewPos.xyz / viewPos.w;}void main() { vec3 viewPos = getViewPosition(v_texCoord); vec3 normal = texture2D(u_normalTexture, v_texCoord).xyz * 2.0 - 1.0; float occlusion = 0.0; float radius = 0.5; for (int i = 0; i < 64; i++) { // 采样点 vec3 samplePos = viewPos + u_samples[i] * radius; // 投影到屏幕空间 vec4 offset = u_projectionMatrix * vec4(samplePos, 1.0); offset.xyz = offset.xyz / offset.w * 0.5 + 0.5; // 获取采样点深度 float sampleDepth = getViewPosition(offset.xy).z; // 范围检查 float rangeCheck = smoothstep(0.0, 1.0, radius / abs(viewPos.z - sampleDepth)); // 如果采样点深度小于当前点深度,则遮挡 occlusion += (sampleDepth >= samplePos.z ? 1.0 : 0.0) * rangeCheck; } occlusion = 1.0 - (occlusion / 64.0); vec3 color = texture2D(u_colorTexture, v_texCoord).rgb; gl_FragColor = vec4(color * occlusion, 1.0);}多效果链式处理class PostProcessChain { constructor(gl, width, height) { this.gl = gl; // 创建两个帧缓冲区用于乒乓渲染 this.fbo1 = new PostProcess(gl, width, height); this.fbo2 = new PostProcess(gl, width, height); this.effects = []; } addEffect(effect) { this.effects.push(effect); } render(sceneRenderFunc) { // 第一步:渲染场景到 FBO1 this.fbo1.beginScene(); sceneRenderFunc(); this.fbo1.endScene(); let readFBO = this.fbo1; let writeFBO = this.fbo2; // 应用每个效果 for (let effect of this.effects) { writeFBO.beginScene(); effect.apply(readFBO.colorTexture); writeFBO.endScene(); // 交换读写缓冲区 [readFBO, writeFBO] = [writeFBO, readFBO]; } // 最后渲染到屏幕 this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null); finalPass.apply(readFBO.colorTexture); }}性能优化建议降低分辨率:后期处理可以使用半分辨率或四分之一分辨率特别适合模糊等效果合并效果:将多个简单效果合并到一个着色器中减少渲染通道使用 Mipmap:模糊效果可以使用 mipmap 快速降采样智能更新:静态场景不需要每帧都进行后期处理可以隔帧更新某些效果移动端优化:减少采样次数使用简单的近似算法
阅读 0·3月6日 21:57

WebGL 中的帧缓冲区(Framebuffer)和离屏渲染是什么?

WebGL 帧缓冲区概述帧缓冲区(Framebuffer)是 WebGL 中用于离屏渲染的目标。与默认的屏幕缓冲区不同,帧缓冲区可以将渲染结果输出到纹理或渲染缓冲区,而不是直接显示在屏幕上。帧缓冲区的组成一个完整的帧缓冲区(FBO)可以包含以下附件:颜色附件(Color Attachment):存储颜色信息,可以是纹理或渲染缓冲区深度附件(Depth Attachment):存储深度值模板附件(Stencil Attachment):存储模板值┌─────────────────────────────────────┐│ Framebuffer │├─────────────────────────────────────┤│ Color Attachment │ Texture/ ││ │ Renderbuffer │├─────────────────────────────────────┤│ Depth Attachment │ Renderbuffer │├─────────────────────────────────────┤│ Stencil Attachment │ Renderbuffer │└─────────────────────────────────────┘创建帧缓冲区基本创建流程// 1. 创建帧缓冲区const framebuffer = gl.createFramebuffer();// 2. 创建颜色附件(纹理)const colorTexture = gl.createTexture();gl.bindTexture(gl.TEXTURE_2D, colorTexture);gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);// 3. 创建深度附件(渲染缓冲区)const depthBuffer = gl.createRenderbuffer();gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height);// 4. 绑定帧缓冲区并附加附件gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorTexture, 0);gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);// 5. 检查帧缓冲区完整性const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);if (status !== gl.FRAMEBUFFER_COMPLETE) { console.error('Framebuffer incomplete:', status);}// 6. 解绑gl.bindFramebuffer(gl.FRAMEBUFFER, null);离屏渲染基本渲染流程function renderToTexture() { // 1. 绑定帧缓冲区(离屏渲染) gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); // 2. 设置视口 gl.viewport(0, 0, framebufferWidth, framebufferHeight); // 3. 清除缓冲区 gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // 4. 渲染场景 drawScene(); // 5. 解绑帧缓冲区(恢复屏幕渲染) gl.bindFramebuffer(gl.FRAMEBUFFER, null); // 6. 恢复视口 gl.viewport(0, 0, canvas.width, canvas.height); // 7. 使用渲染结果(colorTexture 现在包含渲染结果) gl.bindTexture(gl.TEXTURE_2D, colorTexture); drawFullscreenQuad();}纹理 vs 渲染缓冲区| 特性 | 纹理(Texture) | 渲染缓冲区(Renderbuffer) ||------|-----------------|---------------------------|| 可读性 | 可以作为纹理采样 | 不能直接读取 || 性能 | 稍慢 | 更快(针对写入优化) || 用途 | 需要后续处理的颜色附件 | 深度/模板附件 || 灵活性 | 高 | 低 |选择建议颜色附件:通常使用纹理(需要后续采样)深度/模板附件:通常使用渲染缓冲区(性能更好)多重渲染目标(MRT - WebGL 2.0)// WebGL 2.0 支持同时渲染到多个颜色附件const framebuffer = gl.createFramebuffer();gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);// 创建多个颜色附件const textures = [];for (let i = 0; i < 4; i++) { const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, texture, 0 ); textures.push(texture);}// 指定要绘制的附件gl.drawBuffers([ gl.COLOR_ATTACHMENT0, // 输出到纹理 0 gl.COLOR_ATTACHMENT1, // 输出到纹理 1 gl.COLOR_ATTACHMENT2, // 输出到纹理 2 gl.COLOR_ATTACHMENT3 // 输出到纹理 3]);#version 300 eslayout(location = 0) out vec4 gPosition;layout(location = 1) out vec4 gNormal;layout(location = 2) out vec4 gAlbedo;layout(location = 3) out vec4 gMaterial;void main() { gPosition = vec4(worldPos, 1.0); gNormal = vec4(normal, 0.0); gAlbedo = texture(u_diffuseMap, texCoord); gMaterial = vec4(roughness, metallic, ao, 1.0);}常见应用场景1. 后期处理(Post-processing)// 渲染场景到纹理renderToTexture();// 应用后期处理效果applyBloomEffect();applyToneMapping();2. 阴影贴图(Shadow Mapping)// 第一步:从光源视角渲染深度 gl.bindFramebuffer(gl.FRAMEBUFFER, shadowFramebuffer);renderSceneFromLightPerspective();// 第二步:使用深度纹理渲染场景gl.bindFramebuffer(gl.FRAMEBUFFER, null);useShadowMapTexture();renderSceneWithShadows();3. 反射/折射// 渲染环境到立方体贴图for (let face = 0; face < 6; face++) { gl.bindFramebuffer(gl.FRAMEBUFFER, cubeFramebuffer[face]); setupCameraForFace(face); renderEnvironment();}// 使用环境贴图useCubeMapTexture();renderReflectiveObject();4. 延迟渲染(Deferred Rendering)// G-Buffer 渲染 gl.bindFramebuffer(gl.FRAMEBUFFER, gBuffer);gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2]);renderGeometry();// 光照计算 gl.bindFramebuffer(gl.FRAMEBUFFER, null);useGBufferTextures();applyLighting();帧缓冲区管理类class Framebuffer { constructor(gl, width, height, options = {}) { this.gl = gl; this.width = width; this.height = height; this.framebuffer = gl.createFramebuffer(); this.textures = {}; this.renderbuffers = {}; gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); // 创建颜色附件 if (options.color !== false) { this.addColorAttachment(0, options.colorFormat || gl.RGBA); } // 创建深度附件 if (options.depth) { this.addDepthAttachment(options.depthFormat || gl.DEPTH_COMPONENT16); } // 创建模板附件 if (options.stencil) { this.addStencilAttachment(); } // 检查完整性 const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); if (status !== gl.FRAMEBUFFER_COMPLETE) { throw new Error(`Framebuffer incomplete: ${status}`); } gl.bindFramebuffer(gl.FRAMEBUFFER, null); } addColorAttachment(index, internalFormat) { const gl = this.gl; const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, this.width, this.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + index, gl.TEXTURE_2D, texture, 0); this.textures[`color${index}`] = texture; return texture; } addDepthAttachment(internalFormat) { const gl = this.gl; const renderbuffer = gl.createRenderbuffer(); gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer); gl.renderbufferStorage(gl.RENDERBUFFER, internalFormat, this.width, this.height); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderbuffer); this.renderbuffers.depth = renderbuffer; return renderbuffer; } bind() { this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.framebuffer); this.gl.viewport(0, 0, this.width, this.height); } unbind() { this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null); } getTexture(name = 'color0') { return this.textures[name]; } resize(width, height) { this.width = width; this.height = height; // 重新创建纹理和渲染缓冲区... } destroy() { const gl = this.gl; gl.deleteFramebuffer(this.framebuffer); Object.values(this.textures).forEach(t => gl.deleteTexture(t)); Object.values(this.renderbuffers).forEach(r => gl.deleteRenderbuffer(r)); }}// 使用示例const fbo = new Framebuffer(gl, 1024, 1024, { color: true, depth: true});fbo.bind();renderScene();fbo.unbind();// 使用渲染结果const texture = fbo.getTexture('color0');gl.bindTexture(gl.TEXTURE_2D, texture);性能优化建议复用帧缓冲区:避免频繁创建和销毁使用适当尺寸:不要创建过大的帧缓冲区共享深度缓冲区:多个帧缓冲区可以共享同一个深度缓冲区延迟创建:只在需要时创建帧缓冲区使用渲染缓冲区存储深度:比纹理更高效常见问题帧缓冲区不完整const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);switch (status) { case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT: console.error('附件不完整'); break; case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: console.error('缺少附件'); break; case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS: console.error('附件尺寸不匹配'); break; case gl.FRAMEBUFFER_UNSUPPORTED: console.error('不支持的格式组合'); break;}纹理尺寸限制const maxSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);const maxRenderbufferSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE);console.log(`Max texture size: ${maxSize}`);console.log(`Max renderbuffer size: ${maxRenderbufferSize}`);
阅读 0·3月6日 21:57

WebGL 性能优化有哪些常用技巧?

WebGL 性能优化概述WebGL 性能优化是 3D Web 应用开发的关键。由于 JavaScript 和 GPU 之间的通信开销,以及移动设备的资源限制,合理的优化策略能显著提升渲染性能。1. 减少绘制调用(Draw Calls)问题每次 gl.drawArrays 或 gl.drawElements 都有 CPU 到 GPU 的通信开销。优化方案批量绘制(Batching)// 优化前:多次绘制调用for (let mesh of meshes) { gl.bindBuffer(gl.ARRAY_BUFFER, mesh.vbo); gl.drawArrays(gl.TRIANGLES, 0, mesh.vertexCount);}// 优化后:合并到一个缓冲区const mergedBuffer = mergeMeshes(meshes);gl.bindBuffer(gl.ARRAY_BUFFER, mergedBuffer);gl.drawArrays(gl.TRIANGLES, 0, totalVertexCount);实例化渲染(Instanced Rendering)// WebGL 2.0 原生支持// 一次绘制调用渲染多个相同几何体const instanceCount = 1000;gl.drawArraysInstanced(gl.TRIANGLES, 0, vertexCount, instanceCount);2. 减少状态切换问题频繁切换着色器程序、纹理、缓冲区等状态会造成性能开销。优化方案按状态排序// 按着色器程序排序meshes.sort((a, b) => a.program.id - b.program.id);// 按纹理排序meshes.sort((a, b) => a.texture.id - b.texture.id);let currentProgram = null;let currentTexture = null;for (let mesh of meshes) { // 只在需要时切换程序 if (mesh.program !== currentProgram) { gl.useProgram(mesh.program); currentProgram = mesh.program; } // 只在需要时切换纹理 if (mesh.texture !== currentTexture) { gl.bindTexture(gl.TEXTURE_2D, mesh.texture); currentTexture = mesh.texture; } mesh.draw();}使用纹理图集(Texture Atlas)// 将多个小纹理合并为一个大纹理// 减少纹理绑定切换次数const atlasTexture = createTextureAtlas([ 'texture1.png', 'texture2.png', 'texture3.png']);// 在着色器中使用纹理坐标偏移uniform vec2 u_atlasOffset;uniform vec2 u_atlasScale;vec2 atlasCoord = a_texCoord * u_atlasScale + u_atlasOffset;vec4 color = texture2D(u_texture, atlasCoord);3. 优化着色器顶点着色器优化// 优化前:在顶点着色器中进行复杂计算attribute vec3 a_position;uniform mat4 u_modelMatrix;uniform mat4 u_viewMatrix;uniform mat4 u_projectionMatrix;void main() { // 每次顶点都进行矩阵乘法 mat4 mvp = u_projectionMatrix * u_viewMatrix * u_modelMatrix; gl_Position = mvp * vec4(a_position, 1.0);}// 优化后:在 CPU 预计算 MVP 矩阵uniform mat4 u_mvpMatrix;void main() { gl_Position = u_mvpMatrix * vec4(a_position, 1.0);}片段着色器优化// 优化前:复杂的逐像素计算void main() { vec3 lightDir = normalize(u_lightPos - v_worldPos); float diff = max(dot(v_normal, lightDir), 0.0); vec3 diffuse = diff * u_lightColor; // ... 更多计算}// 优化后:顶点着色器计算光照// 顶点着色器varying vec3 v_lightIntensity;void main() { // 在顶点级别计算光照 vec3 lightDir = normalize(u_lightPos - worldPos); float diff = max(dot(normal, lightDir), 0.0); v_lightIntensity = diff * u_lightColor;}// 片段着色器void main() { // 使用插值后的光照强度 gl_FragColor = vec4(v_lightIntensity * textureColor, 1.0);}使用适当精度// 高精度(highp)- 顶点位置、变换矩阵attribute highp vec3 a_position;uniform highp mat4 u_mvpMatrix;// 中精度(mediump)- 颜色、纹理坐标attribute mediump vec2 a_texCoord;varying mediump vec2 v_texCoord;// 低精度(lowp)- 光照计算结果varying lowp vec3 v_lightColor;4. 缓冲区优化使用 VAO 减少状态设置// WebGL 2.0 或扩展支持const vao = gl.createVertexArray();gl.bindVertexArray(vao);// 配置顶点属性(只执行一次)gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);gl.enableVertexAttribArray(0);gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 0, 0);gl.enableVertexAttribArray(1);gl.bindVertexArray(null);// 绘制时只需绑定 VAOgl.bindVertexArray(vao);gl.drawArrays(gl.TRIANGLES, 0, count);使用索引绘制// 优化前:36 个顶点定义一个立方体const vertices = new Float32Array([ // 每个面 6 个顶点,共 6 个面 // 大量重复顶点数据]);// 优化后:8 个顶点 + 36 个索引const vertices = new Float32Array([ // 8 个唯一顶点]);const indices = new Uint16Array([ // 36 个索引定义 12 个三角形]);gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);使用 Interleaved Arrays// 优化前:分离的缓冲区const positions = new Float32Array([/* ... */]);const colors = new Float32Array([/* ... */]);const texCoords = new Float32Array([/* ... */]);// 优化后:交错的顶点数据const vertices = new Float32Array([ // x, y, z, r, g, b, u, v 0, 0, 0, 1, 0, 0, 0, 0, // 顶点 1 1, 0, 0, 0, 1, 0, 1, 0, // 顶点 2 // ...]);// 更好的缓存局部性5. 纹理优化纹理压缩// 使用压缩纹理格式const compressedExtension = gl.getExtension('WEBGL_compressed_texture_s3tc');// 上传压缩纹理数据gl.compressedTexImage2D( gl.TEXTURE_2D, 0, compressedExtension.COMPRESSED_RGBA_S3TC_DXT5_EXT, width, height, 0, compressedData);Mipmap 使用// 启用 mipmap 提高渲染质量和性能gl.generateMipmap(gl.TEXTURE_2D);gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);纹理尺寸优化使用 2 的幂次尺寸(支持 mipmap)避免过大的纹理(内存和带宽开销)根据距离使用不同分辨率的纹理(LOD)6. 遮挡剔除和视锥剔除视锥剔除function isInFrustum(boundingBox, viewProjectionMatrix) { // 将包围盒转换到裁剪空间 // 检查是否在视锥体内 const corners = boundingBox.getCorners(); for (let corner of corners) { const clipPos = transformPoint(corner, viewProjectionMatrix); if (Math.abs(clipPos.x) <= clipPos.w && Math.abs(clipPos.y) <= clipPos.w && 0 <= clipPos.z && clipPos.z <= clipPos.w) { return true; } } return false;}// 只渲染在视锥体内的物体for (let object of scene.objects) { if (isInFrustum(object.boundingBox, vpMatrix)) { object.render(); }}遮挡查询(WebGL 2.0)const query = gl.createQuery();// 开始遮挡查询gl.beginQuery(gl.ANY_SAMPLES_PASSED, query);drawBoundingBox(object);gl.endQuery(gl.ANY_SAMPLES_PASSED);// 检查结果const available = gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE);if (available) { const visible = gl.getQueryParameter(query, gl.QUERY_RESULT) > 0; if (visible) { drawDetailedMesh(object); }}7. 帧缓冲区优化减少分辨率// 在高 DPI 屏幕上使用适当分辨率const dpr = Math.min(window.devicePixelRatio, 2);canvas.width = canvas.clientWidth * dpr;canvas.height = canvas.clientHeight * dpr;延迟渲染优化// G-Buffer 优化:使用适当精度// 位置:RGB16F 或 RGBA16F// 法线:RGB10_A2 或 RGBA8// 材质:RGBA88. JavaScript 优化避免垃圾回收// 优化前:每帧创建新数组function update() { const matrix = new Float32Array(16); // 创建垃圾 // ...}// 优化后:重用数组const matrix = new Float32Array(16);function update() { // 重用 matrix,不创建新对象 mat4.identity(matrix); // ...}使用 TypedArrays// 使用 Float32Array 而不是普通数组const positions = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]);gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);9. 移动端优化减少过度绘制// 从前到后绘制不透明物体gl.enable(gl.DEPTH_TEST);gl.depthFunc(gl.LEQUAL);opaqueObjects.sort((a, b) => b.distance - a.distance);for (let obj of opaqueObjects) { obj.draw();}使用适当精度// 移动端使用 mediump 优化性能precision mediump float;避免复杂着色器减少纹理采样次数避免动态分支简化光照计算10. 性能监控// 使用 EXT_disjoint_timer_query 测量 GPU 时间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);// 获取结果const available = ext.getQueryObjectEXT(query, ext.QUERY_RESULT_AVAILABLE_EXT);if (available) { const timeElapsed = ext.getQueryObjectEXT(query, ext.QUERY_RESULT_EXT); console.log(`GPU time: ${timeElapsed / 1000000} ms`);}总结| 优化方向 | 主要技巧 ||----------|----------|| 绘制调用 | 批量绘制、实例化渲染 || 状态切换 | 状态排序、纹理图集、VAO || 着色器 | 预计算、适当精度、简化计算 || 缓冲区 | 索引绘制、交错数组 || 纹理 | 压缩、mipmap、合理尺寸 || 剔除 | 视锥剔除、遮挡查询 || JavaScript | 避免 GC、TypedArrays |
阅读 0·3月6日 21:57

WebGL 1.0 和 WebGL 2.0 有什么区别?

WebGL 版本概述WebGL 1.0 于 2011 年发布,基于 OpenGL ES 2.0。WebGL 2.0 于 2017 年发布,基于 OpenGL ES 3.0,带来了大量新功能和性能改进。主要区别对比| 特性 | WebGL 1.0 | WebGL 2.0 ||------|-----------|-----------|| 基础规范 | OpenGL ES 2.0 | OpenGL ES 3.0 || 发布年份 | 2011 | 2017 || 着色器版本 | GLSL ES 1.0 | GLSL ES 3.0 || 3D 纹理 | 需要扩展 | 原生支持 || 多重渲染目标(MRT) | 需要扩展 | 原生支持 || 实例化渲染 | 需要扩展 | 原生支持 || 变换反馈 | 不支持 | 支持 || 采样器对象 | 不支持 | 支持 || 顶点数组对象(VAO) | 需要扩展 | 原生支持 || 非2的幂次纹理 | 有限制 | 完全支持 |着色器语言差异WebGL 1.0 (GLSL ES 1.0)// 顶点着色器attribute vec3 a_position;attribute vec2 a_texCoord;uniform mat4 u_mvpMatrix;uniform sampler2D u_texture;varying vec2 v_texCoord;void main() { gl_Position = u_mvpMatrix * vec4(a_position, 1.0); v_texCoord = a_texCoord;}// 片段着色器precision mediump float;varying vec2 v_texCoord;uniform sampler2D u_texture;void main() { gl_FragColor = texture2D(u_texture, v_texCoord);}WebGL 2.0 (GLSL ES 3.0)#version 300 es// 顶点着色器in vec3 a_position; // attribute → inin vec2 a_texCoord;uniform mat4 u_mvpMatrix;out vec2 v_texCoord; // varying → outvoid main() { gl_Position = u_mvpMatrix * vec4(a_position, 1.0); v_texCoord = a_texCoord;}// 片段着色器#version 300 esprecision mediump float;in vec2 v_texCoord; // varying → inuniform sampler2D u_texture;out vec4 fragColor; // gl_FragColor → out 变量void main() { fragColor = texture(u_texture, v_texCoord); // texture2D → texture}着色器语法变化| WebGL 1.0 | WebGL 2.0 | 说明 ||-----------|-----------|------|| attribute | in | 顶点输入 || varying | in/out | 顶点/片段间传递数据 || gl_FragColor | out 变量 | 片段着色器输出 || texture2D() | texture() | 2D 纹理采样 || textureCube() | texture() | 立方体纹理采样 || - | #version 300 es | 版本声明(必需) |WebGL 2.0 新增功能详解1. 3D 纹理// WebGL 2.0 原生支持 3D 纹理const texture3D = gl.createTexture();gl.bindTexture(gl.TEXTURE_3D, texture3D);gl.texImage3D( gl.TEXTURE_3D, // 目标 0, // 级别 gl.RGBA, // 内部格式 width, height, depth, // 深度 0, // 边框 gl.RGBA, gl.UNSIGNED_BYTE, data);#version 300 esuniform sampler3D u_volumeTexture;void main() { vec4 color = texture(u_volumeTexture, vec3(x, y, z));}2. 多重渲染目标(MRT)// 创建帧缓冲区,绑定多个颜色附件const framebuffer = gl.createFramebuffer();gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);// 附加多个纹理const textures = [];for (let i = 0; i < 4; i++) { const texture = gl.createTexture(); // ... 配置纹理 gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, // COLOR_ATTACHMENT0, COLOR_ATTACHMENT1, ... gl.TEXTURE_2D, texture, 0 ); textures.push(texture);}// 指定绘制到哪些附件gl.drawBuffers([ gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2, gl.COLOR_ATTACHMENT3]);#version 300 eslayout(location = 0) out vec4 color0;layout(location = 1) out vec4 color1;layout(location = 2) out vec4 color2;layout(location = 3) out vec4 color3;void main() { color0 = vec4(1.0, 0.0, 0.0, 1.0); color1 = vec4(0.0, 1.0, 0.0, 1.0); color2 = vec4(0.0, 0.0, 1.0, 1.0); color3 = vec4(1.0, 1.0, 1.0, 1.0);}3. 实例化渲染(Instanced Rendering)// WebGL 2.0 原生支持// 绘制 1000 个实例,每个实例使用不同的变换const instanceCount = 1000;gl.drawArraysInstanced(gl.TRIANGLES, 0, vertexCount, instanceCount);// 或使用索引绘制gl.drawElementsInstanced(gl.TRIANGLES, indexCount, gl.UNSIGNED_SHORT, 0, instanceCount);#version 300 esin vec3 a_position;in mat4 a_instanceMatrix; // 实例化矩阵void main() { gl_Position = u_projectionMatrix * u_viewMatrix * a_instanceMatrix * vec4(a_position, 1.0);}4. 变换反馈(Transform Feedback)// 创建变换反馈对象const transformFeedback = gl.createTransformFeedback();// 设置顶点着色器输出const vertexShaderSource = `#version 300 esin vec3 a_position;out vec3 v_newPosition; // 变换后的位置void main() { v_newPosition = a_position * 2.0; // 某种变换}`;// 配置变换反馈const program = gl.createProgram();// ... 编译链接着色器gl.transformFeedbackVaryings( program, ['v_newPosition'], // 要捕获的输出变量 gl.SEPARATE_ATTRIBS // 或 INTERLEAVED_ATTRIBS);// 执行变换反馈gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, transformFeedback);gl.beginTransformFeedback(gl.POINTS);gl.drawArrays(gl.POINTS, 0, count);gl.endTransformFeedback();5. 采样器对象(Sampler Objects)// 将纹理参数从纹理对象分离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);// 绑定采样器到纹理单元gl.bindSampler(0, sampler); // 绑定到纹理单元 06. 顶点数组对象(VAO)原生支持// WebGL 2.0 原生支持,无需扩展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.bindVertexArray(null);// 绘制时只需绑定 VAOgl.bindVertexArray(vao);gl.drawArrays(gl.TRIANGLES, 0, count);新纹理功能非2的幂次纹理完整支持// WebGL 2.0 中,非2的幂次纹理支持所有功能gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);gl.generateMipmap(gl.TEXTURE_2D); // 也支持 mipmap纹理数组const textureArray = gl.createTexture();gl.bindTexture(gl.TEXTURE_2D_ARRAY, textureArray);gl.texImage3D( gl.TEXTURE_2D_ARRAY, 0, gl.RGBA, width, height, layerCount, // 层数 0, gl.RGBA, gl.UNSIGNED_BYTE, data);如何检测 WebGL 2.0 支持function getWebGLContext(canvas) { // 优先尝试 WebGL 2.0 let gl = canvas.getContext('webgl2'); if (gl) { console.log('Using WebGL 2.0'); return gl; } // 回退到 WebGL 1.0 gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); if (gl) { console.log('Using WebGL 1.0'); return gl; } console.error('WebGL not supported'); return null;}浏览器支持情况| 浏览器 | WebGL 1.0 | WebGL 2.0 ||--------|-----------|-----------|| Chrome | ✓ | ✓ (56+) || Firefox | ✓ | ✓ (51+) || Safari | ✓ | ✓ (15+) || Edge | ✓ | ✓ (79+) || IE 11 | ✓ | ✗ |迁移建议渐进增强:先检测 WebGL 2.0 支持,不支持时回退到 1.0着色器版本:为两个版本准备不同的着色器代码功能检测:使用特定功能前检查是否可用性能考虑:WebGL 2.0 功能更强大,但 1.0 兼容性更好
阅读 0·3月6日 21:57