ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

WebGPU 阴影映射管线深度实现:手写 WGSL PCF 柔和阴影着色器

WebGPU 阴影映射管线深度实现:手写 WGSL PCF 柔和阴影着色器 WebGPU 阴影映射管线深度实现手写 WGSL PCF 柔和阴影着色器在 Web 3D 渲染领域阴影是赋予虚拟场景空间纵深感、物体接触感以及真实物理体量最关键的视觉要素。没有阴影的三维模型无论漫反射与高光材质调校得多么逼真往往都像悬浮在空中一样虚假。随着 WebGL 逐渐向 WebGPU 演进现代浏览器终于获得了直接调用现代图形 APIMetal、Vulkan、DirectX 12底层管线的能力。WebGPU 不仅提供了更加显式、低开销的命令编码器Command Encoder更在 WGSLWebGPU Shading Language中原生内置了专门用于阴影比较的硬件加速采样器接口Comparison Sampler。然而手写一套工业级阴影管线远比简单加载模型复杂得多。最基础的阴影映射Shadow Mapping算法会产生令人抓狂的严重锯齿、阴影痤疮Shadow Acne以及漏光浮空Peter Panning。本文将完整手写一套基于 WebGPU 与 WGSL 的双 Pass 渲染管线并实现高质量的**百分比渐近滤波PCFPercentage Closer Filtering**柔和阴影。阴影映射的基础拓扑与两大几何硬伤阴影映射在本质上是一个双 Pass两阶段渲染流程Pass 1深度 Pass将虚拟摄像机放置在光源位置Light Space向场景投射正交投影或透视投影只渲染场景的深度值输出为一张高精度深度纹理Depth TexturePass 2主渲染 Pass在真实摄像机视角下渲染场景。在片段着色器中将当前片元的世界坐标通过“光空间变换矩阵Light View-Projection Matrix”转换到光源裁剪空间换算为归一化设备坐标NDC并与深度纹理中的记录值进行深度比对。如果当前片元的深度大于深度纹理中的值说明该片元处于阴影之中。在这个经典流程中有两个不可避免的数值几何痛点1. 阴影痤疮Shadow Acne由于深度纹理分辨率有限在倾斜表面上一个深度纹理像素Texel覆盖了一段具有斜率的几何平面。当进行深度比对时平面上有一半的点深度会略微大于纹理记录的中心深度另一半略微小于导致表面产生交替出现的黑斑条纹类似痤疮。解决机制引入法线自适应深度偏移Slope-Scaled Depth Bias让偏移量随光线与法线夹角动态缩放$$\text{Bias} \max(0.005 \times (1.0 - \vec{N} \cdot \vec{L}), 0.0005)$$2. 漏光悬浮Peter Panning如果设置的 Bias 偏移量过大物体根部的阴影会被人为推离几何边界导致物体看起来像悬空漂浮。因此偏移量必须极其克制并依赖高质量的滤波来抚平视觉瑕疵。WebGPU 宿主环境深度纹理与比较采样器构建在 TypeScript 中搭建 WebGPU 管线时必须显式声明深度纹理格式并创建一个compare: less的比较采样器// shadowPipeline.ts export function createShadowResources(device: GPUDevice, shadowMapResolution: number 2048) { // 1. 创建光空间深度纹理 const shadowDepthTexture device.createTexture({ size: [shadowMapResolution, shadowMapResolution, 1], usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, format: depth32float, }); // 2. 创建硬件比较采样器 (专用于 textureSampleCompare) const shadowSampler device.createSampler({ compare: less, magFilter: linear, minFilter: linear, }); return { shadowDepthTexture, shadowSampler }; }在 WebGPU 规范中当采样器配置了compare属性时GPU 硬件纹理单元在寻址时会自动执行比较运算并返回 0.0 或 1.0 的浮点权重这是 PCF 性能媲美原生渲染的关键。WGSL 阴影着色器手写实战从硬阴影到 4x4 PCF 滤波在片段着色器中单纯对单个邻域采样会产生强烈的马赛克边缘硬阴影。PCFPercentage Closer Filtering的核心思想是在光空间坐标周围取一组网格核Kernel在一定半径内多次采样比较并取所有采样结果的算术平均值作为遮挡可见度因子Visibility Factor。下面是生产级 WGSL 着色器的完整代码实现// shader.wgsl struct Uniforms { modelMatrix: mat4x4f32, viewProjectionMatrix: mat4x4f32, lightViewProjectionMatrix: mat4x4f32, lightPos: vec3f32, shadowMapSize: f32, // 如 2048.0 }; group(0) binding(0) varuniform uniforms: Uniforms; group(0) binding(1) var shadowMap: texture_depth_2d; group(0) binding(2) var shadowSampler: sampler_comparison; struct VertexInput { location(0) position: vec3f32, location(1) normal: vec3f32, }; struct VertexOutput { builtin(position) clipPosition: vec4f32, location(0) worldPosition: vec3f32, location(1) worldNormal: vec3f32, location(2) shadowPos: vec4f32, }; vertex fn vs_main(in: VertexInput) - VertexOutput { var out: VertexOutput; let worldPos uniforms.modelMatrix * vec4f32(in.position, 1.0); out.worldPosition worldPos.xyz; out.worldNormal normalize((uniforms.modelMatrix * vec4f32(in.normal, 0.0)).xyz); out.clipPosition uniforms.viewProjectionMatrix * worldPos; // 将顶点直接投射到光空间投影平面 out.shadowPos uniforms.lightViewProjectionMatrix * worldPos; return out; } // 核心函数: 4x4 网格 PCF 柔和滤波 fn calculatePCFShadow(shadowCoords: vec4f32, normal: vec3f32, lightDir: vec3f32) - f32 { // 1. 透视除法: 将齐次坐标转为归一化设备坐标 (NDC) let projCoords shadowCoords.xyz / shadowCoords.w; // 2. 将 NDC [-1, 1] 映射到纹理 UV 空间 [0, 1] // 注意 WebGPU 的 Y 轴向下深度 Z 取值范围是 [0, 1] let shadowUV vec2f32( projCoords.x * 0.5 0.5, -projCoords.y * 0.5 0.5 ); // 超出视锥体边界直接不产生阴影 if (shadowUV.x 0.0 || shadowUV.x 1.0 || shadowUV.y 0.0 || shadowUV.y 1.0 || projCoords.z 1.0) { return 1.0; } // 3. 计算基于斜率的自适应 Bias let cosTheta clamp(dot(normal, lightDir), 0.0, 1.0); let bias max(0.004 * (1.0 - cosTheta), 0.0005); let currentDepth projCoords.z - bias; // 4. 执行 4x4 网格 PCF 滤波采样 let texelSize 1.0 / uniforms.shadowMapSize; var shadowTotal 0.0; let sampleCount 16.0; // 遍历 [-1.5, -0.5, 0.5, 1.5] for (var y: f32 -1.5; y 1.5; y 1.0) { for (var x: f32 -1.5; x 1.5; x 1.0) { let offset vec2f32(x, y) * texelSize; // 使用硬件内置比较采样函数 textureSampleCompare // 当 currentDepth 纹理深度时返回 1.0 (可见)否则返回 0.0 (遮挡) shadowTotal textureSampleCompare( shadowMap, shadowSampler, shadowUV offset, currentDepth ); } } return shadowTotal / sampleCount; } fragment fn fs_main(in: VertexOutput) - location(0) vec4f32 { let lightDir normalize(uniforms.lightPos - in.worldPosition); let normal normalize(in.worldNormal); // 计算漫反射光照 let nDotL max(dot(normal, lightDir), 0.0); let baseColor vec3f32(0.85, 0.88, 0.92); let ambient 0.15; // 计算 PCF 阴影遮挡因子 let visibility calculatePCFShadow(in.shadowPos, normal, lightDir); // 合成最终着色 (阴影仅影响漫反射与高光不影响环境光) let lighting ambient (1.0 - ambient) * nDotL * visibility; let finalColor baseColor * lighting; return vec4f32(finalColor, 1.0); }进阶调优泊松圆盘采样与带宽瓶颈防范上述 4x4 的常规网格核采样虽然效果良好但在大半径软阴影Penumbra需求下网格采样容易出现十字形的采样条带。在工业级渲染引擎中通常采用泊松圆盘Poisson Disk分布随机散列采样点并配合屏幕空间噪声进行轻微抖动Jittering能以较少的采样数如 8 到 12 次获得更加自然的半影过渡。显存带宽与 Pass 调度优化准则Pass 1 严禁开启颜色输出在深度渲染阶段WebGPU 的GPURenderPassDescriptor中只需设置depthStencilAttachment将colorAttachments置为空数组。这样 GPU 无需执行任何颜色光栅化和内存写入大幅减少移动端显存带宽消耗。正交投影视锥体的紧密贴合Cascaded Shadow Maps 前置如果平行光的正交投影视锥体过大阴影贴图的一半面积可能被浪费在视口外的空旷区域导致有效区域像素密度骤降。应根据当前摄像机平截头体Frustum的包围盒实时紧凑收缩光空间的left/right/top/bottom范围。通过这一套完整的 WebGPU 深度管线与 WGSL 硬件比较采样实现我们不仅打破了 WebGL 时代阴影采样的性能瓶颈更在浏览器中呈现出了如同端游级引擎般顺滑、自然且极具真实感的柔和光影体验。
返回列表