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

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

3月7日 12:04

WebGL 立方体贴图概述

立方体贴图(Cubemap)是一种特殊的纹理,由 6 张独立的 2D 纹理组成,分别对应立方体的 6 个面。它使用 3D 方向向量进行采样,常用于实现天空盒、环境反射和折射等效果。

立方体贴图的结构

立方体贴图由 6 个面组成:

shell
┌─────────┐ │ +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) 进行采样,选择哪个面取决于哪个分量的绝对值最大:

shell
如果 |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);
标签:WebGL