5月29日 01:40

How is shadow implemented in WebGL?

WebGL Shadow Overview

Shadows are an important technique in 3D rendering for increasing realism. The main method for implementing shadows in WebGL is Shadow Mapping, which is an image-based shadow technique.

Shadow Mapping Principle

The core idea of shadow mapping:

  1. Step 1: Render the scene from the light's perspective to generate a Depth Map
  2. Step 2: Render the scene from the camera's perspective, transform each fragment to light space, and compare depth values
  3. Judgment: If the fragment's depth in light space is greater than the value in the depth map, the fragment is in shadow
shell
Light perspective rendering → Depth Map Camera perspective rendering → Light space coordinates → Depth comparison → Shadow judgment

Shadow Mapping Implementation Steps

Step 1: Generate Depth Map

Vertex Shader (Depth Generation)

glsl
// shadow_vertex.glsl attribute vec3 a_position; uniform mat4 u_lightSpaceMatrix; // Light projection matrix × Light view matrix × Model matrix void main() { gl_Position = u_lightSpaceMatrix * vec4(a_position, 1.0); }

Fragment Shader (Depth Generation)

glsl
// shadow_fragment.glsl // WebGL 1.0 - Need to manually write depth value precision mediump float; varying vec4 v_position; void main() { // Manually calculate depth value and encode to color float depth = v_position.z / v_position.w; depth = depth * 0.5 + 0.5; // Convert to [0, 1] // Use RGBA to encode depth value (improve precision) 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 - Can directly use depth attachment // Fragment shader can be empty, depth written automatically

JavaScript Code

javascript
// Create depth map framebuffer function createDepthFramebuffer(gl, width, height) { const framebuffer = gl.createFramebuffer(); // Create depth texture 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 ); // Set texture parameters 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); // Bind to framebuffer gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, 0 ); // WebGL 1.0 needs color attachment even if not writing color 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 }; } // Render depth map function renderDepthMap(gl, shadowProgram, scene, lightSpaceMatrix) { // Bind shadow framebuffer gl.bindFramebuffer(gl.FRAMEBUFFER, shadowFramebuffer); gl.viewport(0, 0, shadowMapSize, shadowMapSize); // Clear depth buffer gl.clear(gl.DEPTH_BUFFER_BIT); // Use depth shader gl.useProgram(shadowProgram); // Render each object in scene for (let object of scene.objects) { // Calculate light space matrix 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); } // Unbind gl.bindFramebuffer(gl.FRAMEBUFFER, null); }

Step 2: Render Scene with Shadows

Vertex Shader

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 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; // Transform to light space v_lightSpacePos = u_lightSpaceMatrix * vec4(a_position, 1.0); gl_Position = u_projectionMatrix * u_viewMatrix * worldPos; }

Fragment Shader

glsl
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; // Decode depth value (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); } // Calculate shadow float calculateShadow(vec4 lightSpacePos) { // Perspective division vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w; // Convert to [0, 1] range projCoords = projCoords * 0.5 + 0.5; // Get current fragment depth from light's perspective float currentDepth = projCoords.z; // Get depth value from depth map float closestDepth = texture2D(u_shadowMap, projCoords.xy).r; // Shadow bias (solves shadow acne problem) float bias = 0.005; // Compare depths float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0; // Check if outside shadow map range if (projCoords.z > 1.0) { shadow = 0.0; } return shadow; } // PCF soft shadow 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 sampling 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; // Ambient vec3 ambient = 0.3 * color; // Diffuse 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; // Calculate shadow float shadow = calculateShadowPCF(v_lightSpacePos); // Combine lighting (shadow areas only keep ambient) vec3 lighting = ambient + (1.0 - shadow) * (diffuse); gl_FragColor = vec4(lighting * color, 1.0); }

Common Shadow Problems and Solutions

1. Shadow Acne

Problem: Striped self-shadow artifacts on surfaces

Cause: Depth precision limitations and floating-point errors

Solution:

glsl
// Add shadow bias float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005); float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;

2. Peter Panning

Problem: Shadows separate from objects, appearing to float

Cause: Bias value too large

Solution:

  • Only enable front face culling when rendering depth map
  • Use smaller bias value
javascript
// Enable front face culling when rendering depth map gl.enable(gl.CULL_FACE); gl.cullFace(gl.FRONT); // Cull front faces, only render back faces // Restore when rendering normal scene gl.cullFace(gl.BACK);

3. Shadow Aliasing

Problem: Jagged edges on shadows

Solution:

  • Use PCF (Percentage Closer Filtering) soft shadows
  • Increase shadow map resolution
  • Use Cascaded Shadow Maps (CSM)

PCF Soft Shadow Implementation

glsl
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)); // Sample surrounding pixels 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 sampling (more natural soft shadows) vec2 poissonDisk[16] = vec2[]( vec2(-0.94201624, -0.39906216), vec2(0.94558609, -0.76890725), // ... more sample points ); 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

For large scenes, use multiple shadow maps with different resolutions:

javascript
// Create multiple cascades const cascades = [ { near: 0.1, far: 10, resolution: 2048 }, { near: 10, far: 50, resolution: 1024 }, { near: 50, far: 200, resolution: 512 } ]; // Select appropriate cascade based on distance function getCascadeIndex(viewSpaceZ) { for (let i = 0; i < cascades.length; i++) { if (viewSpaceZ < cascades[i].far) { return i; } } return cascades.length - 1; }

Point Light Shadows (Cubemap Shadows)

javascript
// Create cubemap 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 ); } // Render 6 faces const shadowTransforms = [ // Projection matrices for +X, -X, +Y, -Y, +Z, -Z directions ];

Performance Optimization Suggestions

  1. Shadow Map Resolution:

    • Mobile: 512x512 or 1024x1024
    • Desktop: 2048x2048 or higher
  2. Update Frequency:

    • Static light sources can generate shadow map only once
    • Dynamic light sources update every frame
  3. Shadow Distance:

    • Limit shadow rendering distance
    • Use cascaded shadow maps
  4. Soft Shadow Performance:

    • PCF 3x3 is a balance between performance and quality
    • Poisson Disk sampling has better quality but higher performance cost
标签:WebGL