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

WebGL

WebGL(Web Graphics Library)是一种 JavaScript API,用于在任何兼容的网页浏览器中不使用插件渲染2D和3D图形。它是基于OpenGL ES的规范,旨在在Web平台上提供OpenGL的性能和功能。通过 WebGL,开发者可以为网页应用程序创建复杂的可视化效果、游戏、可视化数据和各种交互式图形体验。
WebGL
查看更多相关内容
WebGL 渲染管线的工作流程是什么?WebGL 渲染管线(Rendering Pipeline)是一系列将 3D 顶点数据转换为 2D 屏幕像素的处理阶段。理解渲染管线对于优化 WebGL 应用性能至关重要。 ## 渲染管线的各个阶段 ### 1. 顶点处理阶段(Vertex Processing) #### 顶点着色器(Vertex Shader) * **输入**:顶点位置、颜色、纹理坐标、法线等属性 * **处理**: * 坐标变换(模型矩阵、视图矩阵、投影矩阵) * 顶点光照计算 * 纹理坐标变换 * **输出**:裁剪空间坐标(Clip Space Coordinates) ```glsl // 顶点着色器示例 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) * **输入**:插值后的顶点属性 * **处理**: * 纹理采样 * 光照计算 * 颜色混合 * **输出**:最终像素颜色 ```glsl // 片段着色器示例 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 渲染流程图 ``` 顶点数据 → 顶点着色器 → 图元装配 → 裁剪 → 透视除法 → 视口变换 ↓ 帧缓冲区 ← 逐片段操作 ← 片段着色器 ← 光栅化/插值 ← 屏幕映射 ``` ## 性能优化要点 1. **减少绘制调用**:合并网格,使用实例化渲染 2. **优化顶点着色器**:避免复杂计算 3. **减少片段着色器复杂度**:特别是移动端 4. **合理使用深度测试**:从前到后绘制不透明物体 5. **避免过度绘制**:使用遮挡查询 ## WebGL 1.0 vs WebGL 2.0 管线差异 | 特性 | WebGL 1.0 | WebGL 2.0 | | ------ | --------- | --------- | | 变换反馈 | 不支持 | 支持 | | 多重渲染目标 | 需要扩展 | 原生支持 | | 3D 纹理 | 需要扩展 | 原生支持 | | 实例化渲染 | 需要扩展 | 原生支持 | ​
服务端 · 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) └─────────┘ ``` ## 创建立方体贴图 ### 基本创建流程 ```javascript // 创建立方体贴图 const cubemap = gl.createTexture(); gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubemap); // 6 个面的图片 URL const 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); ``` ### 程序化生成立方体贴图 ```javascript // 创建纯色立方体贴图 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; } ``` ## 在着色器中使用立方体贴图 ### 顶点着色器 ```glsl 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; } ``` ### 片段着色器 ```glsl 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) 天空盒用于渲染远处的环境背景,给人一种无限大的感觉。 ```glsl // 天空盒顶点着色器 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); } ``` ```javascript // 渲染天空盒 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) 使用立方体贴图模拟光滑表面的反射效果。 ```glsl // 反射顶点着色器 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) 模拟光线穿过透明物体的折射效果。 ```glsl // 折射片段着色器 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) 模拟真实世界中反射强度随视角变化的效应。 ```glsl // 菲涅尔反射片段着色器 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) 实时生成立方体贴图用于反射。 ```javascript // 动态生成环境贴图 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 ) ``` ## 性能优化建议 1. **预过滤环境贴图**: - 为不同粗糙度预计算模糊的立方体贴图 - 使用 mipmap 级别存储不同粗糙度的反射 2. **立方体贴图压缩**: - 使用压缩纹理格式减少内存占用 - DXT、ETC、PVRTC 等格式 3. **动态环境贴图优化**: - 降低分辨率(如 128x128) - 减少更新频率(如每 10 帧更新一次) - 只更新可见物体的反射 4. **LOD 系统**: - 远处物体使用低分辨率立方体贴图 - 近处物体使用高分辨率立方体贴图 ## 常见问题 ### 接缝问题 立方体贴图面与面之间可能出现接缝。 **解决方案**: ```javascript // 使用 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); ``` ### 图片方向问题 不同面的图片可能需要翻转。 **解决方案**: - 使用图像编辑软件预先调整 - 或在着色器中翻转纹理坐标 ```glsl // 翻转 Y 坐标 vec2 flippedCoord = vec2(texCoord.x, 1.0 - texCoord.y); ```
服务端 · 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 ```javascript // 1. 创建缓冲区 const vbo = gl.createBuffer(); // 2. 绑定缓冲区(指定当前操作的缓冲区类型) gl.bindBuffer(gl.ARRAY_BUFFER, vbo); // 3. 上传数据到 GPU const 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); // 启用颜色属性 ``` ### 缓冲区使用模式 ```javascript // 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 ```javascript // 顶点数据(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 // 第二个三角形 ]); // 创建并绑定 VBO gl.bindBuffer(gl.ARRAY_BUFFER, vbo); gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); // 创建并绑定 IBO const 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 存储了所有顶点属性的配置状态,包括: - 启用的顶点属性 - 每个属性的配置(大小、类型、步长、偏移) - 绑定的 VBO ### WebGL 2.0 / WebGL 1.0 + OES_vertex_array_object 扩展 ```javascript // 创建 VAO const 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); // 解绑 VAO gl.bindVertexArray(null); // 绘制时只需绑定 VAO gl.bindVertexArray(vao); gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); ``` ## 完整示例代码 ```javascript 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(); ``` ## 性能优化建议 1. **减少状态切换**:使用 VAO 减少绘制前的配置开销 2. **合并缓冲区**:将多个小网格合并到一个大缓冲区 3. **使用索引绘制**:减少顶点数据重复 4. **选择合适的绘制模式**: - `STATIC_DRAW`:静态几何体 - `DYNAMIC_DRAW`:频繁更新的数据 - `STREAM_DRAW`:每帧都更新的数据 5. **批量绘制**:使用实例化渲染(Instanced Rendering)绘制多个相同对象
服务端 · 3月7日 12:04
WebGL 中的雾效(Fog)是如何实现的?## WebGL 雾效概述 雾效(Fog)是一种模拟大气散射效果的渲染技术,使远处的物体逐渐融入到背景颜色中。雾效不仅能增加场景的真实感,还能隐藏远处的裁剪边界,优化性能。 ## 雾效的基本原理 雾效的核心思想是根据物体与相机的距离,在物体颜色和雾颜色之间进行线性或指数插值: ``` 最终颜色 = 物体颜色 × (1 - 雾因子) + 雾颜色 × 雾因子 ``` ## 雾的类型 ### 1. 线性雾(Linear Fog) 雾的浓度随距离线性增加。 **公式**: ``` fogFactor = (end - distance) / (end - start) ``` - `start`:雾开始距离 - `end`:雾完全覆盖距离 - `distance`:片段到相机的距离 ```glsl // 顶点着色器 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) ``` ```glsl 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)²) ``` ```glsl 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); } ``` ## 完整的雾效实现 ### 顶点着色器 ```glsl 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; } ``` ### 片段着色器 ```glsl 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 控制 ```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) 模拟根据高度变化的雾效,如山谷中的雾。 ```glsl // 顶点着色器 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); } ``` ## 雾效与光照结合 ```glsl 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. 顶点级雾效计算 当顶点数较少时,可以在顶点着色器中计算雾因子: ```glsl // 顶点着色器 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. 使用深度纹理 在后处理阶段应用雾效: ```javascript // 1. 渲染场景到颜色纹理和深度纹理 renderSceneToTextures(); // 2. 后处理阶段应用雾效 applyFogPostProcess(); ``` ```glsl // 后处理雾效着色器 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 轴 | 适合山谷、水面等场景 | ## 实际应用建议 1. **选择合适的雾类型**: - 大多数场景使用指数或指数平方雾 - 需要精确控制雾边界时使用线性雾 2. **调整雾的颜色**: - 通常与天空盒或背景色一致 - 可以随时间变化模拟昼夜效果 3. **性能考虑**: - 移动端建议使用顶点级雾效 - 复杂场景可以使用后处理雾效 4. **与其他效果结合**: - 雾效可以与体积光、大气散射结合 - 注意雾效对透明物体的影响
服务端 · 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 | ``` ```javascript 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 × M** ### M - 模型矩阵(Model Matrix) **作用**:将顶点从**模型空间**(局部空间)转换到**世界空间** ```javascript // 模型矩阵 = 平移 × 旋转 × 缩放 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) **作用**:将顶点从**世界空间**转换到**相机空间**(观察空间) ```javascript // 使用 lookAt 函数创建视图矩阵 const viewMatrix = mat4.create(); mat4.lookAt( viewMatrix, [0, 0, 5], // 相机位置(眼睛) [0, 0, 0], // 观察目标点 [0, 1, 0] // 上方向向量 ); ``` **视图矩阵的本质**: - 将世界坐标系原点移动到相机位置 - 旋转坐标系使相机朝向 -Z 方向 - 相机位于原点,看向 -Z 轴方向 ### P - 投影矩阵(Projection Matrix) **作用**:将顶点从**相机空间**转换到**裁剪空间** #### 透视投影(Perspective Projection) 模拟人眼视觉效果,近大远小 ```javascript const projectionMatrix = mat4.create(); mat4.perspective( projectionMatrix, Math.PI / 4, // 视野角度(FOV) canvas.width / canvas.height, // 宽高比 0.1, // 近平面 100.0 // 远平面 ); ``` #### 正交投影(Orthographic Projection) 保持物体大小不变,常用于 2D 游戏或 CAD 软件 ```javascript mat4.ortho( projectionMatrix, -2, 2, // 左右 -2, 2, // 下上 0.1, 100 // 近远 ); ``` ## 坐标空间转换流程 ``` 模型空间(局部空间) ↓ [模型矩阵 M] 世界空间 ↓ [视图矩阵 V] 相机空间(观察空间) ↓ [投影矩阵 P] 裁剪空间 ↓ [透视除法] 标准化设备坐标(NDC) ↓ [视口变换] 屏幕空间 ``` ## 顶点着色器中的 MVP 应用 ```glsl // 顶点着色器 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 中的矩阵计算 ```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` 必须为 `false` ### 3. 齐次坐标 使用 4D 向量 `(x, y, z, w)`: - 顶点位置:`w = 1` - 方向向量:`w = 0`(不受平移影响) ### 4. 性能优化 - 在 CPU 端预计算 MVP 矩阵,而不是在着色器中分别相乘 - 使用 uniform 传递预计算的矩阵
服务端 · 3月6日 21:58
WebGL 中的光照模型有哪些?如何实现 Phong 光照模型?## WebGL 光照模型概述 光照模型用于模拟光线与物体表面的交互,是 3D 渲染中实现真实感的关键技术。WebGL 中常用的光照模型包括:环境光(Ambient)、漫反射(Diffuse)和镜面反射(Specular)。 ## 基本光照模型 ### 1. 环境光(Ambient Lighting) 模拟场景中无处不在的间接光照,不考虑光源位置和方向。 ```glsl vec3 ambient = ambientStrength * lightColor; ``` ### 2. 漫反射(Diffuse Lighting) 模拟光线照射到粗糙表面后向各个方向均匀反射的效果。遵循**朗伯余弦定律**。 ```glsl // 计算光线方向与法线的夹角 float diff = max(dot(normal, lightDir), 0.0); vec3 diffuse = diff * lightColor; ``` ### 3. 镜面反射(Specular Lighting) 模拟光线在光滑表面的定向反射,产生高光效果。 ```glsl // 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 光照模型是三种光照成分的组合: **最终颜色 = 环境光 + 漫反射 + 镜面反射** ### 顶点着色器 ```glsl 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 模型) ```glsl 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 的改进版本,使用半角向量代替反射向量,计算更高效。 ```glsl 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); } ``` ## 法线矩阵的计算 ```javascript // 法线矩阵是模型矩阵的逆转置矩阵的左上 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 的 mat3 const normalMatrix = mat3.create(); mat3.fromMat4(normalMatrix, modelMatrix); mat3.invert(normalMatrix, normalMatrix); mat3.transpose(normalMatrix, normalMatrix); ``` ## 多光源处理 ```glsl #define MAX_LIGHTS 4 struct 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) ```glsl // 方向光只有方向,没有位置 uniform vec3 u_lightDirection; // 光线方向 vec3 lightDir = normalize(-u_lightDirection); // 指向光源 float diff = max(dot(normal, lightDir), 0.0); ``` ### 点光源(Point Light) ```glsl // 点光源有位置,向四面八方发射 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) ```glsl // 聚光灯有位置、方向和角度限制 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); ``` ## 材质属性 ```glsl 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 着色 | |------|-------------|------------| | **计算位置** | 顶点着色器 | 片段着色器 | | **质量** | 较低(插值) | 较高(逐像素) | | **性能** | 更快 | 较慢 | | **高光** | 可能不准确 | 准确 | ```glsl // 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); } ``` ## 性能优化建议 1. **顶点着色器 vs 片段着色器**: - 顶点数 < 片段数时,在顶点着色器计算 - 需要高质量光照时,在片段着色器计算 2. **使用 Blinn-Phong**: - 比 Phong 更高效(避免计算 reflect) - 视觉效果相似 3. **限制光源数量**: - 移动端建议 1-2 个光源 - 桌面端建议 4-8 个光源 4. **使用延迟渲染**: - 大量光源时使用 - 避免对每个光源遍历所有片段
服务端 · 3月6日 21:57
WebGL 中的后期处理(Post-processing)是如何实现的?## WebGL 后期处理概述 后期处理(Post-processing)是在场景渲染完成后,对渲染结果进行图像处理的技术。通过后期处理可以实现各种视觉效果,如模糊、辉光、色调映射、抗锯齿等。 ## 后期处理的基本原理 后期处理的核心流程: 1. 将场景渲染到纹理(离屏渲染) 2. 对纹理进行各种图像处理 3. 将处理后的结果渲染到屏幕 ``` 场景渲染 → 颜色纹理 → 后期处理着色器 → 屏幕 ↓ 深度纹理(可选) ``` ## 基本后期处理框架 ### 创建后期处理所需的资源 ```javascript 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); } } ``` ### 后期处理顶点着色器 ```glsl 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) ```glsl 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) #### 高斯模糊 ```glsl precision mediump float; varying vec2 v_texCoord; uniform sampler2D u_texture; uniform vec2 u_texelSize; // 1.0 / textureSize uniform 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; } ``` #### 双通道模糊(优化性能) ```javascript // 先水平模糊 horizontalBlurProgram.setUniform('u_direction', [1, 0]); postProcess.apply(horizontalBlurProgram); // 再垂直模糊 verticalBlurProgram.setUniform('u_direction', [0, 1]); postProcess.apply(verticalBlurProgram); ``` ### 3. 边缘检测(Edge Detection) ```glsl 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) ```glsl // 提取高亮部分 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) ```glsl 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. 色彩调整 ```glsl 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) ```glsl 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); } ``` ## 多效果链式处理 ```javascript 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); } } ``` ## 性能优化建议 1. **降低分辨率**: - 后期处理可以使用半分辨率或四分之一分辨率 - 特别适合模糊等效果 2. **合并效果**: - 将多个简单效果合并到一个着色器中 - 减少渲染通道 3. **使用 Mipmap**: - 模糊效果可以使用 mipmap 快速降采样 4. **智能更新**: - 静态场景不需要每帧都进行后期处理 - 可以隔帧更新某些效果 5. **移动端优化**: - 减少采样次数 - 使用简单的近似算法
服务端 · 3月6日 21:57
WebGL 中的帧缓冲区(Framebuffer)和离屏渲染是什么?## WebGL 帧缓冲区概述 帧缓冲区(Framebuffer)是 WebGL 中用于离屏渲染的目标。与默认的屏幕缓冲区不同,帧缓冲区可以将渲染结果输出到纹理或渲染缓冲区,而不是直接显示在屏幕上。 ## 帧缓冲区的组成 一个完整的帧缓冲区(FBO)可以包含以下附件: 1. **颜色附件(Color Attachment)**:存储颜色信息,可以是纹理或渲染缓冲区 2. **深度附件(Depth Attachment)**:存储深度值 3. **模板附件(Stencil Attachment)**:存储模板值 ``` ┌─────────────────────────────────────┐ │ Framebuffer │ ├─────────────────────────────────────┤ │ Color Attachment │ Texture/ │ │ │ Renderbuffer │ ├─────────────────────────────────────┤ │ Depth Attachment │ Renderbuffer │ ├─────────────────────────────────────┤ │ Stencil Attachment │ Renderbuffer │ └─────────────────────────────────────┘ ``` ## 创建帧缓冲区 ### 基本创建流程 ```javascript // 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); ``` ## 离屏渲染 ### 基本渲染流程 ```javascript 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) ```javascript // 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 ]); ``` ```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 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) ```javascript // 渲染场景到纹理 renderToTexture(); // 应用后期处理效果 applyBloomEffect(); applyToneMapping(); ``` ### 2. 阴影贴图(Shadow Mapping) ```javascript // 第一步:从光源视角渲染深度 gl.bindFramebuffer(gl.FRAMEBUFFER, shadowFramebuffer); renderSceneFromLightPerspective(); // 第二步:使用深度纹理渲染场景 gl.bindFramebuffer(gl.FRAMEBUFFER, null); useShadowMapTexture(); renderSceneWithShadows(); ``` ### 3. 反射/折射 ```javascript // 渲染环境到立方体贴图 for (let face = 0; face < 6; face++) { gl.bindFramebuffer(gl.FRAMEBUFFER, cubeFramebuffer[face]); setupCameraForFace(face); renderEnvironment(); } // 使用环境贴图 useCubeMapTexture(); renderReflectiveObject(); ``` ### 4. 延迟渲染(Deferred Rendering) ```javascript // 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(); ``` ## 帧缓冲区管理类 ```javascript 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); ``` ## 性能优化建议 1. **复用帧缓冲区**:避免频繁创建和销毁 2. **使用适当尺寸**:不要创建过大的帧缓冲区 3. **共享深度缓冲区**:多个帧缓冲区可以共享同一个深度缓冲区 4. **延迟创建**:只在需要时创建帧缓冲区 5. **使用渲染缓冲区存储深度**:比纹理更高效 ## 常见问题 ### 帧缓冲区不完整 ```javascript 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; } ``` ### 纹理尺寸限制 ```javascript 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}`); ```
服务端 · 3月6日 21:57
WebGL 性能优化有哪些常用技巧?## WebGL 性能优化概述 WebGL 性能优化是 3D Web 应用开发的关键。由于 JavaScript 和 GPU 之间的通信开销,以及移动设备的资源限制,合理的优化策略能显著提升渲染性能。 ## 1. 减少绘制调用(Draw Calls) ### 问题 每次 `gl.drawArrays` 或 `gl.drawElements` 都有 CPU 到 GPU 的通信开销。 ### 优化方案 #### 批量绘制(Batching) ```javascript // 优化前:多次绘制调用 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) ```javascript // WebGL 2.0 原生支持 // 一次绘制调用渲染多个相同几何体 const instanceCount = 1000; gl.drawArraysInstanced(gl.TRIANGLES, 0, vertexCount, instanceCount); ``` ## 2. 减少状态切换 ### 问题 频繁切换着色器程序、纹理、缓冲区等状态会造成性能开销。 ### 优化方案 #### 按状态排序 ```javascript // 按着色器程序排序 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) ```javascript // 将多个小纹理合并为一个大纹理 // 减少纹理绑定切换次数 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. 优化着色器 ### 顶点着色器优化 ```glsl // 优化前:在顶点着色器中进行复杂计算 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); } ``` ### 片段着色器优化 ```glsl // 优化前:复杂的逐像素计算 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); } ``` ### 使用适当精度 ```glsl // 高精度(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 减少状态设置 ```javascript // 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); // 绘制时只需绑定 VAO gl.bindVertexArray(vao); gl.drawArrays(gl.TRIANGLES, 0, count); ``` ### 使用索引绘制 ```javascript // 优化前: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 ```javascript // 优化前:分离的缓冲区 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. 纹理优化 ### 纹理压缩 ```javascript // 使用压缩纹理格式 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 使用 ```javascript // 启用 mipmap 提高渲染质量和性能 gl.generateMipmap(gl.TEXTURE_2D); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); ``` ### 纹理尺寸优化 - 使用 2 的幂次尺寸(支持 mipmap) - 避免过大的纹理(内存和带宽开销) - 根据距离使用不同分辨率的纹理(LOD) ## 6. 遮挡剔除和视锥剔除 ### 视锥剔除 ```javascript 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) ```javascript 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. 帧缓冲区优化 ### 减少分辨率 ```javascript // 在高 DPI 屏幕上使用适当分辨率 const dpr = Math.min(window.devicePixelRatio, 2); canvas.width = canvas.clientWidth * dpr; canvas.height = canvas.clientHeight * dpr; ``` ### 延迟渲染优化 ```javascript // G-Buffer 优化:使用适当精度 // 位置:RGB16F 或 RGBA16F // 法线:RGB10_A2 或 RGBA8 // 材质:RGBA8 ``` ## 8. JavaScript 优化 ### 避免垃圾回收 ```javascript // 优化前:每帧创建新数组 function update() { const matrix = new Float32Array(16); // 创建垃圾 // ... } // 优化后:重用数组 const matrix = new Float32Array(16); function update() { // 重用 matrix,不创建新对象 mat4.identity(matrix); // ... } ``` ### 使用 TypedArrays ```javascript // 使用 Float32Array 而不是普通数组 const positions = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]); gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW); ``` ## 9. 移动端优化 ### 减少过度绘制 ```javascript // 从前到后绘制不透明物体 gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); opaqueObjects.sort((a, b) => b.distance - a.distance); for (let obj of opaqueObjects) { obj.draw(); } ``` ### 使用适当精度 ```glsl // 移动端使用 mediump 优化性能 precision mediump float; ``` ### 避免复杂着色器 - 减少纹理采样次数 - 避免动态分支 - 简化光照计算 ## 10. 性能监控 ```javascript // 使用 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 |
服务端 · 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) ```glsl // 顶点着色器 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) ```glsl #version 300 es // 顶点着色器 in vec3 a_position; // attribute → in in vec2 a_texCoord; uniform mat4 u_mvpMatrix; out vec2 v_texCoord; // varying → out void main() { gl_Position = u_mvpMatrix * vec4(a_position, 1.0); v_texCoord = a_texCoord; } // 片段着色器 #version 300 es precision mediump float; in vec2 v_texCoord; // varying → in uniform 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 纹理 ```javascript // 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 ); ``` ```glsl #version 300 es uniform sampler3D u_volumeTexture; void main() { vec4 color = texture(u_volumeTexture, vec3(x, y, z)); } ``` ### 2. 多重渲染目标(MRT) ```javascript // 创建帧缓冲区,绑定多个颜色附件 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 ]); ``` ```glsl #version 300 es layout(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) ```javascript // WebGL 2.0 原生支持 // 绘制 1000 个实例,每个实例使用不同的变换 const instanceCount = 1000; gl.drawArraysInstanced(gl.TRIANGLES, 0, vertexCount, instanceCount); // 或使用索引绘制 gl.drawElementsInstanced(gl.TRIANGLES, indexCount, gl.UNSIGNED_SHORT, 0, instanceCount); ``` ```glsl #version 300 es in 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) ```javascript // 创建变换反馈对象 const transformFeedback = gl.createTransformFeedback(); // 设置顶点着色器输出 const vertexShaderSource = `#version 300 es in 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) ```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); // 绑定采样器到纹理单元 gl.bindSampler(0, sampler); // 绑定到纹理单元 0 ``` ### 6. 顶点数组对象(VAO)原生支持 ```javascript // 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); // 绘制时只需绑定 VAO gl.bindVertexArray(vao); gl.drawArrays(gl.TRIANGLES, 0, count); ``` ## 新纹理功能 ### 非2的幂次纹理完整支持 ```javascript // 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 ``` ### 纹理数组 ```javascript 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 支持 ```javascript 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 | ✓ | ✗ | ## 迁移建议 1. **渐进增强**:先检测 WebGL 2.0 支持,不支持时回退到 1.0 2. **着色器版本**:为两个版本准备不同的着色器代码 3. **功能检测**:使用特定功能前检查是否可用 4. **性能考虑**:WebGL 2.0 功能更强大,但 1.0 兼容性更好
服务端 · 3月6日 21:57