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

How is shadow implemented in WebGL?

3月6日 21:57

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