WebGL 中的阴影(Shadow)是如何实现的?
WebGL 阴影概述阴影是 3D 渲染中增加真实感的重要技术。WebGL 中实现阴影的主要方法是阴影贴图(Shadow Mapping),它是一种基于图像的阴影技术。阴影贴图原理阴影贴图的核心思想:第一步:从光源视角渲染场景,生成深度贴图(Depth Map)第二步:从相机视角渲染场景,将每个片段转换到光源空间,比较深度值判断:如果片段在光源空间的深度大于深度贴图中的值,则该片段在阴影中光源视角渲染 → 深度贴图 ↓相机视角渲染 → 光源空间坐标 → 深度比较 → 阴影判断阴影贴图实现步骤第一步:生成深度贴图顶点着色器(深度生成)// shadow_vertex.glslattribute vec3 a_position;uniform mat4 u_lightSpaceMatrix; // 光源投影矩阵 × 光源视图矩阵 × 模型矩阵void main() { gl_Position = u_lightSpaceMatrix * vec4(a_position, 1.0);}片段着色器(深度生成)// shadow_fragment.glsl// WebGL 1.0 - 需要手动写入深度值precision mediump float;varying vec4 v_position;void main() { // 手动计算深度值并编码到颜色 float depth = v_position.z / v_position.w; depth = depth * 0.5 + 0.5; // 转换到 [0, 1] // 使用 RGBA 编码深度值(提高精度) vec4 encodedDepth; encodedDepth.r = depth; encodedDepth.g = fract(depth * 256.0); encodedDepth.b = fract(depth * 65536.0); encodedDepth.a = fract(depth * 16777216.0); gl_FragColor = encodedDepth;}// WebGL 2.0 - 可以直接使用深度附件// 片段着色器可以为空,深度自动写入JavaScript 代码// 创建深度贴图帧缓冲区function createDepthFramebuffer(gl, width, height) { const framebuffer = gl.createFramebuffer(); // 创建深度纹理 const depthTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, depthTexture); gl.texImage2D( gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT, // WebGL 2.0 // gl.DEPTH_COMPONENT16, // WebGL 1.0 width, height, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_SHORT, null ); // 设置纹理参数 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 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, framebuffer); gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, 0 ); // WebGL 1.0 需要颜色附件,即使不写入颜色 if (!isWebGL2) { const colorBuffer = gl.createRenderbuffer(); gl.bindRenderbuffer(gl.RENDERBUFFER, colorBuffer); gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA4, width, height); gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, colorBuffer ); } gl.bindFramebuffer(gl.FRAMEBUFFER, null); return { framebuffer, depthTexture };}// 渲染深度贴图function renderDepthMap(gl, shadowProgram, scene, lightSpaceMatrix) { // 绑定阴影帧缓冲区 gl.bindFramebuffer(gl.FRAMEBUFFER, shadowFramebuffer); gl.viewport(0, 0, shadowMapSize, shadowMapSize); // 清除深度缓冲区 gl.clear(gl.DEPTH_BUFFER_BIT); // 使用深度着色器 gl.useProgram(shadowProgram); // 渲染场景中的每个物体 for (let object of scene.objects) { // 计算光源空间矩阵 const modelMatrix = object.getModelMatrix(); const lightSpaceMatrix = mat4.create(); mat4.multiply(lightSpaceMatrix, lightProjectionMatrix, lightViewMatrix); mat4.multiply(lightSpaceMatrix, lightSpaceMatrix, modelMatrix); gl.uniformMatrix4fv( gl.getUniformLocation(shadowProgram, 'u_lightSpaceMatrix'), false, lightSpaceMatrix ); object.draw(gl); } // 解绑 gl.bindFramebuffer(gl.FRAMEBUFFER, null);}第二步:渲染带阴影的场景顶点着色器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 mat4 u_lightSpaceMatrix;varying vec3 v_worldPos;varying vec3 v_normal;varying vec2 v_texCoord;varying vec4 v_lightSpacePos;void main() { vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0); v_worldPos = worldPos.xyz; v_normal = mat3(u_modelMatrix) * a_normal; v_texCoord = a_texCoord; // 转换到光源空间 v_lightSpacePos = u_lightSpaceMatrix * vec4(a_position, 1.0); gl_Position = u_projectionMatrix * u_viewMatrix * worldPos;}片段着色器precision mediump float;varying vec3 v_worldPos;varying vec3 v_normal;varying vec2 v_texCoord;varying vec4 v_lightSpacePos;uniform sampler2D u_shadowMap;uniform vec3 u_lightPos;uniform vec3 u_viewPos;uniform vec3 u_lightColor;// 解码深度值(WebGL 1.0)float decodeDepth(vec4 rgba) { const vec4 bitShift = vec4(1.0, 1.0/256.0, 1.0/65536.0, 1.0/16777216.0); return dot(rgba, bitShift);}// 计算阴影float calculateShadow(vec4 lightSpacePos) { // 透视除法 vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w; // 转换到 [0, 1] 范围 projCoords = projCoords * 0.5 + 0.5; // 获取当前片段在光源视角的深度 float currentDepth = projCoords.z; // 获取深度贴图中的深度值 float closestDepth = texture2D(u_shadowMap, projCoords.xy).r; // 阴影偏移(解决阴影痤疮问题) float bias = 0.005; // 比较深度 float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0; // 检查是否在阴影贴图范围外 if (projCoords.z > 1.0) { shadow = 0.0; } return shadow;}// PCF 软阴影float calculateShadowPCF(vec4 lightSpacePos) { vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w; projCoords = projCoords * 0.5 + 0.5; float currentDepth = projCoords.z; float bias = 0.005; float shadow = 0.0; // 3x3 PCF 采样 vec2 texelSize = 1.0 / vec2(1024.0, 1024.0); for (int x = -1; x <= 1; ++x) { for (int y = -1; y <= 1; ++y) { float pcfDepth = texture2D(u_shadowMap, projCoords.xy + vec2(x, y) * texelSize).r; shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0; } } shadow /= 9.0; if (projCoords.z > 1.0) { shadow = 0.0; } return shadow;}void main() { vec3 color = texture2D(u_texture, v_texCoord).rgb; // 环境光 vec3 ambient = 0.3 * color; // 漫反射 vec3 lightDir = normalize(u_lightPos - v_worldPos); vec3 normal = normalize(v_normal); float diff = max(dot(normal, lightDir), 0.0); vec3 diffuse = diff * u_lightColor; // 计算阴影 float shadow = calculateShadowPCF(v_lightSpacePos); // 组合光照(阴影区域只保留环境光) vec3 lighting = ambient + (1.0 - shadow) * (diffuse); gl_FragColor = vec4(lighting * color, 1.0);}阴影常见问题及解决方案1. 阴影痤疮(Shadow Acne)问题:表面出现条纹状自阴影伪影原因:深度精度限制和浮点数误差解决方案:// 添加阴影偏移float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;2. 彼得潘效应(Peter Panning)问题:阴影与物体分离,看起来像在漂浮原因:偏移值过大解决方案:只在渲染深度贴图时启用正面剔除使用较小的偏移值// 渲染深度贴图时启用正面剔除gl.enable(gl.CULL_FACE);gl.cullFace(gl.FRONT); // 剔除正面,只渲染背面// 渲染正常场景时恢复gl.cullFace(gl.BACK);3. 阴影锯齿问题:阴影边缘出现锯齿解决方案:使用 PCF(Percentage Closer Filtering)软阴影增加阴影贴图分辨率使用级联阴影贴图(CSM)PCF 软阴影实现float calculateShadowPCF(vec4 lightSpacePos, int sampleRadius) { vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w; projCoords = projCoords * 0.5 + 0.5; float currentDepth = projCoords.z; float bias = 0.005; float shadow = 0.0; vec2 texelSize = 1.0 / vec2(textureSize(u_shadowMap, 0)); // 采样周围像素 for (int x = -sampleRadius; x <= sampleRadius; ++x) { for (int y = -sampleRadius; y <= sampleRadius; ++y) { vec2 offset = vec2(float(x), float(y)) * texelSize; float closestDepth = texture(u_shadowMap, projCoords.xy + offset).r; shadow += currentDepth - bias > closestDepth ? 1.0 : 0.0; } } int sampleCount = (sampleRadius * 2 + 1) * (sampleRadius * 2 + 1); shadow /= float(sampleCount); return shadow;}// Poisson Disk 采样(更自然的软阴影)vec2 poissonDisk[16] = vec2[]( vec2(-0.94201624, -0.39906216), vec2(0.94558609, -0.76890725), // ... 更多采样点);float calculateShadowPoisson(vec4 lightSpacePos) { vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w; projCoords = projCoords * 0.5 + 0.5; float currentDepth = projCoords.z; float bias = 0.005; float shadow = 0.0; for (int i = 0; i < 16; i++) { float closestDepth = texture(u_shadowMap, projCoords.xy + poissonDisk[i] / 700.0).r; shadow += currentDepth - bias > closestDepth ? 1.0 : 0.0; } return shadow / 16.0;}级联阴影贴图(Cascaded Shadow Maps)对于大场景,使用多个不同分辨率的阴影贴图:// 创建多个级联const cascades = [ { near: 0.1, far: 10, resolution: 2048 }, { near: 10, far: 50, resolution: 1024 }, { near: 50, far: 200, resolution: 512 }];// 根据距离选择合适的级联function getCascadeIndex(viewSpaceZ) { for (let i = 0; i < cascades.length; i++) { if (viewSpaceZ < cascades[i].far) { return i; } } return cascades.length - 1;}点光源阴影(立方体贴图阴影)// 创建立方体贴图const depthCubemap = gl.createTexture();gl.bindTexture(gl.TEXTURE_CUBE_MAP, depthCubemap);for (let i = 0; i < 6; i++) { gl.texImage2D( gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, gl.DEPTH_COMPONENT, shadowSize, shadowSize, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_SHORT, null );}// 渲染 6 个面const shadowTransforms = [ // +X, -X, +Y, -Y, +Z, -Z 方向的投影矩阵];性能优化建议阴影贴图分辨率:移动端:512x512 或 1024x1024桌面端:2048x2048 或更高更新频率:静态光源可以只生成一次阴影贴图动态光源每帧更新阴影距离:限制阴影渲染距离使用级联阴影贴图软阴影性能:PCF 3x3 是性能和质量的平衡Poisson Disk 采样质量更好但性能开销大