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);