3.2 阴影 (Shadows)


文档摘要

3.2 阴影 (Shadows) 第三章:WebGL 高级技术领域 - 3.2 阴影 (Shadows) 引言 在 WebGL 渲染中,阴影是增强场景真实感和沉浸感的至关重要的元素。没有阴影,即使是最精细的模型和纹理,场景也会显得扁平、缺乏深度和立体感。阴影能够揭示物体之间的空间关系,增强光照效果,并为观众提供更丰富的视觉信息。 3.2.1 阴影的重要性与挑战 阴影之所以重要,是因为它们模拟了真实世界中光线被物体阻挡后产生的效果。阴影可以: 增强空间感和深度感: 阴影帮助我们感知物体之间的相对位置和距离,使场景更具立体感。 提升真实感: 自然界中阴影是光照不可或缺的一部分,添加阴影能显著提升渲染场景的真实度。 强调光照效果: 阴影与光照相互作用,共同塑造场景的氛围和情绪。

3.2 阴影 (Shadows)

第三章:WebGL 高级技术领域 - 3.2 阴影 (Shadows)

引言

在 WebGL 渲染中,阴影是增强场景真实感和沉浸感的至关重要的元素。没有阴影,即使是最精细的模型和纹理,场景也会显得扁平、缺乏深度和立体感。阴影能够揭示物体之间的空间关系,增强光照效果,并为观众提供更丰富的视觉信息。

3.2.1 阴影的重要性与挑战

阴影之所以重要,是因为它们模拟了真实世界中光线被物体阻挡后产生的效果。阴影可以:

  • 增强空间感和深度感: 阴影帮助我们感知物体之间的相对位置和距离,使场景更具立体感。

  • 提升真实感: 自然界中阴影是光照不可或缺的一部分,添加阴影能显著提升渲染场景的真实度。

  • 强调光照效果: 阴影与光照相互作用,共同塑造场景的氛围和情绪。

  • 提供视觉线索: 阴影可以帮助观众理解场景的结构和布局。

然而,在 WebGL 中实现高质量的实时阴影并非易事,它面临着以下挑战:

  • 计算复杂度: 阴影的计算需要额外的渲染过程和复杂的数学运算,会增加 GPU 的负担,影响性能。

  • 精度问题: 阴影的生成和渲染过程中容易出现精度问题,导致阴影边缘锯齿、阴影失真等瑕疵。

  • 实时性要求: WebGL 应用通常需要保持流畅的帧率,实时阴影的计算必须足够高效,才能满足实时渲染的要求。

  • 各种阴影技术的权衡: 不同的阴影技术各有优缺点,需要根据具体应用场景和性能需求进行选择。

3.2.2 阴影技术概述

WebGL 中实现阴影的技术主要可以分为以下几类:

  • 阴影贴图 (Shadow Mapping): 目前最主流的实时阴影技术,通过从光源的角度渲染深度图,然后将场景中的片段深度与深度图进行比较,判断片段是否处于阴影中。

  • 阴影体 (Shadow Volumes): 一种基于几何体的阴影技术,通过计算物体投射的阴影体与视线的交点来判断阴影。阴影体技术能产生精确的硬阴影,但计算成本较高,尤其是在复杂场景中。

  • 平面阴影 (Planar Shadows): 一种简化的阴影技术,将阴影投影到预定义的平面上(例如地面)。平面阴影实现简单,性能开销小,但效果有限,通常只适用于简单的场景。

  • 光线追踪阴影 (Ray Traced Shadows): 一种更高级的阴影技术,通过追踪光线路径来精确计算阴影。光线追踪阴影能产生高质量的软阴影和更真实的阴影效果,但计算成本极高,通常不适用于实时渲染,更多用于离线渲染或高端硬件上的实时应用。

在本篇文章中,我们将重点讲解 阴影贴图 (Shadow Mapping) 技术,因为它在性能、效果和实现复杂度之间取得了良好的平衡,是 WebGL 中最常用和最实用的阴影解决方案。

3.2.3 阴影贴图 (Shadow Mapping) 详解

阴影贴图 (Shadow Mapping) 是一种两阶段渲染技术,其核心思想是:从光源的角度渲染场景,生成深度图,然后将场景中的片段深度与深度图进行比较,判断该片段是否处于阴影中。

3.2.3.1 阴影贴图的原理

阴影贴图的过程可以分为两个主要的渲染阶段:

阶段一:深度图生成 (Depth Map Generation) - 光源视角渲染

  1. 设置光源视角: 我们需要创建一个虚拟的相机,并将它放置在光源的位置,朝向光源照射的方向。这个相机被称为 光照相机 (Light Camera)

  2. 光源视角渲染场景: 使用光照相机渲染整个场景,但此时我们只关心场景中每个像素的深度信息,即物体距离光源的距离。颜色信息被忽略。

  3. 生成深度图: 渲染结果被存储在一个特殊的纹理中,这个纹理被称为 深度贴图 (Depth Map)阴影贴图 (Shadow Map)。深度贴图中的每个像素值代表了从光源视角看到的最近的物体到光源的距离。

mermaid graph TD

A[阶段一:深度图生成] --> B{设置光照相机视角}; B --> C{光源视角渲染场景 (仅深度信息)}; C --> D[生成深度贴图]; D --> E[深度贴图纹理];

阶段二:阴影计算与场景渲染 - 摄像机视角渲染

  1. 摄像机视角正常渲染场景: 使用正常的摄像机视角渲染场景,进行颜色、纹理、光照等计算。

  2. 将片段坐标转换到光源空间: 对于场景中的每个片段 (Fragment),我们需要将其世界坐标转换到光源相机的裁剪空间坐标系下。这个转换过程需要使用 光照相机的视图矩阵投影矩阵,以及 模型矩阵

  3. 深度比较: 将转换后的片段坐标的深度值(光源空间深度值)与深度贴图中对应位置的深度值进行比较。

    • 如果片段的光源空间深度值大于深度贴图中的深度值: 说明该片段距离光源更远,被遮挡,因此处于阴影中。

    • 如果片段的光源空间深度值小于或等于深度贴图中的深度值: 说明该片段距离光源更近或相等,没有被遮挡,因此处于光照中。

  4. 应用阴影效果: 根据深度比较的结果,对处于阴影中的片段降低亮度或颜色,模拟阴影效果。

mermaid graph TD

F[阶段二:阴影计算与场景渲染] --> G{摄像机视角正常渲染场景}; G --> H{片段坐标转换到光源空间}; H --> I{深度比较 (片段深度 vs 深度贴图)}; I -- 是 (片段深度 > 深度贴图) --> J[片段处于阴影中]; I -- 否 (片段深度 <= 深度贴图) --> K[片段处于光照中]; J --> L[应用阴影效果 (降低亮度/颜色)]; K --> L; L --> M[最终渲染结果 (带阴影)];

3.2.3.2 阴影贴图的实现步骤 (代码实践)

以下是一个使用 WebGL 实现阴影贴图的详细步骤,包括代码示例和解释。

1. 设置 WebGL 环境和场景

首先,你需要初始化 WebGL 上下文,创建 canvas,设置视口,并准备好场景数据,包括模型、材质、光源等。

const canvas = document.getElementById('webglCanvas'); const gl = canvas.getContext('webgl'); if (!gl) { console.error('WebGL not supported!'); return; } gl.viewport(0, 0, canvas.width, canvas.height); gl.enable(gl.DEPTH_TEST); // 启用深度测试 // ... 假设你已经有了场景数据和模型加载代码 ... // 例如:模型顶点数据、法线数据、材质属性、光源信息等

2. 创建深度贴图纹理和帧缓冲对象 (Framebuffer Object)

深度贴图需要存储在纹理中,并且我们需要使用帧缓冲对象来将渲染结果渲染到纹理中。

const shadowMapSize = 1024; // 阴影贴图分辨率,可以根据需求调整 // 创建深度纹理 const depthTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, depthTexture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT32F, shadowMapSize, shadowMapSize, 0, gl.DEPTH_COMPONENT, gl.FLOAT, 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); // 创建帧缓冲对象 const depthFramebuffer = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, depthFramebuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, 0); // 将深度纹理附加到帧缓冲对象 gl.bindFramebuffer(gl.FRAMEBUFFER, null); // 解绑帧缓冲对象

代码解释:

  • shadowMapSize: 定义了深度贴图的分辨率。分辨率越高,阴影质量越好,但性能开销也越大。

  • gl.createTexture(): 创建纹理对象。

  • gl.bindTexture(): 绑定纹理对象,后续的纹理操作将作用于该纹理。

  • gl.texImage2D(): 初始化纹理数据。

    • gl.DEPTH_COMPONENT32F: 指定纹理内部格式为深度分量,使用 32 位浮点数存储深度值。

    • gl.DEPTH_COMPONENT: 指定纹理数据格式为深度分量。

    • gl.FLOAT: 指定纹理数据类型为浮点数。

    • null: 深度纹理不需要初始数据,设置为 null

  • gl.texParameteri(): 设置纹理参数,例如采样方式和边缘处理。gl.NEAREST 使用最近邻采样,性能较好。gl.CLAMP_TO_EDGE 将纹理边缘像素延伸到边缘外,防止采样超出范围。

  • gl.createFramebuffer(): 创建帧缓冲对象。

  • gl.bindFramebuffer(): 绑定帧缓冲对象,后续的渲染操作将渲染到该帧缓冲对象的目标上。

  • gl.framebufferTexture2D(): 将纹理附加到帧缓冲对象。

    • gl.DEPTH_ATTACHMENT: 指定将纹理作为深度附件附加到帧缓冲对象。

3. 编写 Shader 程序 (深度渲染 Shader 和 阴影渲染 Shader)

我们需要编写两个 Shader 程序:

  • 深度渲染 Shader (Depth Shader): 用于从光源视角渲染深度图,只输出深度信息。

  • 阴影渲染 Shader (Shadow Shader): 用于正常的场景渲染,并根据深度贴图计算阴影。

深度渲染 Shader (Depth Shader)

顶点着色器 (depth_vertex.glsl)

#version 300 es in vec3 a_position; uniform mat4 u_lightViewProjectionMatrix; uniform mat4 u_modelMatrix; void main() { gl_Position = u_lightViewProjectionMatrix * u_modelMatrix * vec4(a_position, 1.0); }

片段着色器 (depth_fragment.glsl)

#version 300 es void main() { // 片段着色器不需要输出颜色,只需要输出深度值,WebGL 默认会处理深度值 }

阴影渲染 Shader (Shadow Shader)

顶点着色器 (shadow_vertex.glsl)

#version 300 es in vec3 a_position; in vec3 a_normal; uniform mat4 u_modelMatrix; uniform mat4 u_viewMatrix; uniform mat4 u_projectionMatrix; uniform mat4 u_lightViewProjectionMatrix; // 光源的 VP 矩阵 out vec3 v_normalWorld; out vec4 v_shadowCoord; // 用于阴影计算的坐标 void main() { vec4 worldPosition = u_modelMatrix * vec4(a_position, 1.0); gl_Position = u_projectionMatrix * u_viewMatrix * worldPosition; v_normalWorld = mat3(u_modelMatrix) * a_normal; v_shadowCoord = u_lightViewProjectionMatrix * worldPosition; // 将世界坐标转换到光源裁剪空间 }

片段着色器 (shadow_fragment.glsl)

#version 300 es precision mediump float; in vec3 v_normalWorld; in vec4 v_shadowCoord; uniform sampler2D u_shadowMap; uniform vec3 u_lightDirection; uniform vec3 u_lightColor; uniform vec3 u_ambientColor; uniform vec3 u_diffuseColor; uniform vec3 u_specularColor; uniform float u_shininess; uniform vec3 u_cameraPosition; out vec4 fragColor; float getShadowFactor() { // 将光源空间坐标转换到纹理坐标 (0 ~ 1) vec3 shadowCoord = v_shadowCoord.xyz / v_shadowCoord.w * 0.5 + 0.5; shadowCoord.z += 0.005; // 深度偏移 (Bias),解决阴影自遮挡问题 if (shadowCoord.x < 0.0 || shadowCoord.x > 1.0 || shadowCoord.y < 0.0 || shadowCoord.y > 1.0 || shadowCoord.z > 1.0) { return 1.0; // 超出阴影贴图范围,不应用阴影 } // 采样深度贴图 float shadowDepth = texture(u_shadowMap, shadowCoord.xy).r; // 深度比较,判断是否处于阴影中 if (shadowCoord.z > shadowDepth) { return 0.5; // 处于阴影中,阴影强度可以调整 } else { return 1.0; // 没有阴影 } } void main() { vec3 normal = normalize(v_normalWorld); vec3 lightDir = normalize(-u_lightDirection); // 光源方向指向光源 // 环境光 vec3 ambient = u_ambientColor * u_diffuseColor; // 漫反射光 float diffFactor = max(dot(normal, lightDir), 0.0); vec3 diffuse = u_diffuseColor * u_lightColor * diffFactor; // 高光 vec3 viewDir = normalize(u_cameraPosition - gl_FragCoord.xyz); // 观察方向 vec3 reflectDir = reflect(-lightDir, normal); // 反射方向 float specFactor = pow(max(dot(viewDir, reflectDir), 0.0), u_shininess); vec3 specular = u_specularColor * u_lightColor * specFactor; // 阴影因子 float shadowFactor = getShadowFactor(); // 最终颜色 = (环境光 + 漫反射光 + 高光) * 阴影因子 vec3 finalColor = (ambient + diffuse + specular) * shadowFactor; fragColor = vec4(finalColor, 1.0); }

代码解释:

  • 深度渲染 Shader: 非常简单,顶点着色器将顶点转换到光源裁剪空间,片段着色器不需要输出颜色,WebGL 会自动处理深度值。

  • 阴影渲染 Shader:

    • 顶点着色器:

      • v_shadowCoord: 将顶点世界坐标转换到光源裁剪空间,传递给片段着色器,用于后续的阴影计算。
    • 片段着色器:

      • u_shadowMap: 接收深度贴图纹理。

      • getShadowFactor() 函数:

        • 将光源空间坐标 v_shadowCoord 转换为纹理坐标 (0~1)。

        • shadowCoord.z += 0.005;: 深度偏移 (Bias),用于解决阴影自遮挡问题。由于深度贴图的精度限制,以及浮点数计算的误差,容易出现片段自身被判断为处于阴影中的情况,导致阴影错误 (Shadow Acne)。深度偏移可以将片段的深度值稍微向光源方向偏移,避免自遮挡。偏移量 0.005 需要根据场景和深度贴图分辨率调整。

        • texture(u_shadowMap, shadowCoord.xy).r: 采样深度贴图,获取深度值。

        • 深度比较:如果片段的深度值大于深度贴图中的深度值,则返回阴影因子 0.5 (表示处于阴影中),否则返回 1.0 (表示没有阴影)。阴影强度 0.5 可以根据需求调整。

      • main() 函数:

        • 计算环境光、漫反射光、高光。

        • 调用 getShadowFactor() 获取阴影因子。

        • 将光照结果乘以阴影因子,应用阴影效果。

4. 设置 Uniform 变量和属性

在 JavaScript 代码中,需要获取 Shader 程序中 Uniform 变量和属性的位置,并设置相应的值。

// 深度渲染 Shader 程序 const depthProgram = createProgramFromSources(gl, [depthVertexShaderSource, depthFragmentShaderSource]); const depthProgramUniformLocations = { lightViewProjectionMatrix: gl.getUniformLocation(depthProgram, 'u_lightViewProjectionMatrix'), modelMatrix: gl.getUniformLocation(depthProgram, 'u_modelMatrix'), }; const depthProgramAttributeLocations = { position: gl.getAttribLocation(depthProgram, 'a_position'), }; // 阴影渲染 Shader 程序 const shadowProgram = createProgramFromSources(gl, [shadowVertexShaderSource, shadowFragmentShaderSource]); const shadowProgramUniformLocations = { modelMatrix: gl.getUniformLocation(shadowProgram, 'u_modelMatrix'), viewMatrix: gl.getUniformLocation(shadowProgram, 'u_viewMatrix'), projectionMatrix: gl.getUniformLocation(shadowProgram, 'u_projectionMatrix'), lightViewProjectionMatrix: gl.getUniformLocation(shadowProgram, 'u_lightViewProjectionMatrix'), shadowMap: gl.getUniformLocation(shadowProgram, 'u_shadowMap'), lightDirection: gl.getUniformLocation(shadowProgram, 'u_lightDirection'), lightColor: gl.getUniformLocation(shadowProgram, 'u_lightColor'), ambientColor: gl.getUniformLocation(shadowProgram, 'u_ambientColor'), diffuseColor: gl.getUniformLocation(shadowProgram, 'u_diffuseColor'), specularColor: gl.getUniformLocation(shadowProgram, 'u_specularColor'), shininess: gl.getUniformLocation(shadowProgram, 'u_shininess'), cameraPosition: gl.getUniformLocation(shadowProgram, 'u_cameraPosition'), }; const shadowProgramAttributeLocations = { position: gl.getAttribLocation(shadowProgram, 'a_position'), normal: gl.getAttribLocation(shadowProgram, 'a_normal'), };

5. 渲染循环 (Render Loop)

在渲染循环中,我们需要执行两个渲染阶段:深度图渲染和场景渲染。

function render() { requestAnimationFrame(render); // 更新摄像机和光源参数 (例如:摄像机位置、旋转,光源方向等) updateCamera(); updateLight(); // ----- 阶段一:深度图渲染 ----- gl.bindFramebuffer(gl.FRAMEBUFFER, depthFramebuffer); // 绑定帧缓冲对象,渲染到深度贴图 gl.viewport(0, 0, shadowMapSize, shadowMapSize); // 设置视口大小为阴影贴图分辨率 gl.clear(gl.DEPTH_BUFFER_BIT); // 清除深度缓冲 gl.useProgram(depthProgram); // 使用深度渲染 Shader // 设置光源的 View 和 Projection 矩阵 const lightViewMatrix = lookAt(lightPosition, lightTarget, upVector); // 根据光源位置和目标点计算 View 矩阵 const lightProjectionMatrix = perspective(fov, aspectRatio, near, far); // 根据光源视角参数计算 Projection 矩阵 const lightViewProjectionMatrix = multiply(lightProjectionMatrix, lightViewMatrix); // 渲染场景中的每个模型 for (const model of models) { // 设置模型矩阵 gl.uniformMatrix4fv(depthProgramUniformLocations.modelMatrix, false, model.modelMatrix); // 设置光源 VP 矩阵 gl.uniformMatrix4fv(depthProgramUniformLocations.lightViewProjectionMatrix, false, lightViewProjectionMatrix); // 设置顶点属性并绘制模型 (假设模型数据已经加载到 Buffer 中) gl.bindBuffer(gl.ARRAY_BUFFER, model.positionBuffer); gl.vertexAttribPointer(depthProgramAttributeLocations.position, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(depthProgramAttributeLocations.position); gl.drawArrays(gl.TRIANGLES, 0, model.vertexCount); } // ----- 阶段二:场景渲染 (带阴影) ----- gl.bindFramebuffer(gl.FRAMEBUFFER, null); // 解绑帧缓冲对象,渲染到默认帧缓冲 (屏幕) gl.viewport(0, 0, canvas.width, canvas.height); // 设置视口大小为 canvas 尺寸 gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // 清除颜色缓冲和深度缓冲 gl.useProgram(shadowProgram); // 使用阴影渲染 Shader // 设置摄像机的 View 和 Projection 矩阵 const viewMatrix = camera.getViewMatrix(); const projectionMatrix = camera.getProjectionMatrix(); // 设置光源参数 gl.uniform3fv(shadowProgramUniformLocations.lightDirection, lightDirection); gl.uniform3fv(shadowProgramUniformLocations.lightColor, lightColor); gl.uniform3fv(shadowProgramUniformLocations.ambientColor, ambientColor); gl.uniform3fv(shadowProgramUniformLocations.diffuseColor, diffuseColor); gl.uniform3fv(shadowProgramUniformLocations.specularColor, specularColor); gl.uniform1f(shadowProgramUniformLocations.shininess, shininess); gl.uniform3fv(shadowProgramUniformLocations.cameraPosition, cameraPosition); // 设置光源 VP 矩阵 gl.uniformMatrix4fv(shadowProgramUniformLocations.lightViewProjectionMatrix, false, lightViewProjectionMatrix); // 绑定深度贴图纹理到纹理单元 0 gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, depthTexture); gl.uniform1i(shadowProgramUniformLocations.shadowMap, 0); // 传递纹理单元 0 给 Shader // 渲染场景中的每个模型 for (const model of models) { // 设置模型矩阵 gl.uniformMatrix4fv(shadowProgramUniformLocations.modelMatrix, false, model.modelMatrix); // 设置 View 和 Projection 矩阵 gl.uniformMatrix4fv(shadowProgramUniformLocations.viewMatrix, false, viewMatrix); gl.uniformMatrix4fv(shadowProgramUniformLocations.projectionMatrix, false, projectionMatrix); // 设置顶点属性并绘制模型 gl.bindBuffer(gl.ARRAY_BUFFER, model.positionBuffer); gl.vertexAttribPointer(shadowProgramAttributeLocations.position, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(shadowProgramAttributeLocations.position); gl.bindBuffer(gl.ARRAY_BUFFER, model.normalBuffer); gl.vertexAttribPointer(shadowProgramAttributeLocations.normal, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(shadowProgramAttributeLocations.normal); gl.drawArrays(gl.TRIANGLES, 0, model.vertexCount); } } render(); // 开始渲染循环

代码解释:

  • 渲染循环的两个阶段: 代码清晰地分为深度图渲染阶段和场景渲染阶段。

  • 深度图渲染阶段:

    • 绑定深度帧缓冲对象 depthFramebuffer

    • 设置视口大小为 shadowMapSize

    • 使用 depthProgram (深度渲染 Shader)。

    • 计算光源的 View 和 Projection 矩阵。

    • 遍历场景中的模型,设置模型矩阵和光源 VP 矩阵,并绘制模型。

  • 场景渲染阶段:

    • 解绑帧缓冲对象,渲染到默认帧缓冲 (屏幕)。

    • 设置视口大小为 canvas 尺寸。

    • 使用 shadowProgram (阴影渲染 Shader)。

    • 设置摄像机的 View 和 Projection 矩阵。

    • 设置光源参数、材质参数、摄像机位置等 Uniform 变量。

    • 设置光源 VP 矩阵。

    • 绑定深度贴图纹理到纹理单元 0,并传递纹理单元索引 0 给 Shader 的 u_shadowMap Uniform 变量。

    • 遍历场景中的模型,设置模型矩阵、View 矩阵、Projection 矩阵,并绘制模型。

6. 矩阵计算辅助函数 (示例)

代码中使用了 lookAt, perspective, multiply 等矩阵计算函数,你需要根据你使用的数学库或自行实现这些函数。以下是一些简化的示例代码 (可以使用 glMatrix 或 cuon-matrix.js 等成熟的库):

// 简化的 lookAt 矩阵计算 (需要根据实际需求完善) function lookAt(eye, target, up) { /* ... 实现 lookAt 矩阵计算 ... */ } // 简化的 perspective 矩阵计算 (需要根据实际需求完善) function perspective(fov, aspectRatio, near, far) { /* ... 实现 perspective 矩阵计算 ... */ } // 简化的矩阵相乘 (需要根据实际需求完善) function multiply(matrixA, matrixB) { /* ... 实现矩阵相乘 ... */ } // 创建 Shader 程序 (假设 createShader 和 createProgramFromSources 函数已经实现) function createShader(gl, type, source) { /* ... 创建 Shader ... */ } function createProgramFromSources(gl, sources) { /* ... 创建 Program ... */ }

3.2.4 阴影贴图的优化和改进

3.2.4.1 深度偏移 (Bias)

深度偏移 (Bias) 是解决阴影自遮挡 (Shadow Acne) 问题的关键技术。由于深度贴图的精度限制和浮点数计算误差,容易出现片段自身被判断为处于阴影中的情况,导致阴影表面出现噪点或条纹状的错误阴影。深度偏移通过将片段的深度值稍微向光源方向偏移,避免自遮挡。

在片段着色器中,我们添加了深度偏移:

shadowCoord.z += 0.005; // 深度偏移

深度偏移量 0.005 需要根据场景和深度贴图分辨率进行调整。过小的偏移量可能无法完全消除自遮挡,过大的偏移量可能导致阴影与物体分离 (Peter-Panning)。

3.2.4.2 阴影边缘柔化 (PCF - Percentage Closer Filtering)

原始的阴影贴图会产生硬阴影,阴影边缘锯齿明显。为了获得更柔和、更自然的阴影效果,可以使用 百分比渐近滤波 (Percentage Closer Filtering, PCF) 技术。

PCF 的基本思想是:在采样深度贴图时,不是只采样一个像素,而是采样多个像素,然后根据采样结果计算阴影因子。

例如,我们可以采样 3x3 或 5x5 的深度贴图区域,然后统计有多少个采样点处于阴影中,根据阴影点的比例来计算最终的阴影因子。

修改片段着色器中的 getShadowFactor() 函数:


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U