WebGPU 玻璃材质实现指南:从渲染管线到混合状态全流程解析
2026/8/28 7:14:47 网站建设 项目流程

第一次在 WebGPU 里尝试做玻璃材质,我以为难点在 shader:折射怎么算、反射怎么叠加、高光怎么压住。真正跑起来才发现,这些反而是水到渠成的事。最难的,是把整条渲染链路想清楚:先用哪个 Pass 渲染背景,玻璃物体放哪一步画,混合状态怎么配,折射采样从哪里取纹理,多个半透明物体之间的顺序谁来管。WebGPU Glass Material 这个主题,表面上是视觉效果问题,实际上是一个渲染管线组织问题。

如果你也打算在浏览器里做玻璃质感的产品展示,或者想把一个实时渲染场景里的透明物体变得“更像玻璃”,这篇文章可以当作一条从零开始的路径。我不会只贴一个 shader,因为只贴 shader 解决不了问题。真正要建立的是:先理解玻璃在实时渲染里是什么,再理解 WebGPU 把它变成了什么,最后才是动手把流程搭起来。

1. 先重新理解:玻璃材质不是写个 shader,而是搭一条渲染链路

1.1 玻璃材质表面看是视觉技巧,实质是采样与合成的顺序问题

玻璃在真实世界里之所以难模拟,是因为你看到的颜色由几部分叠加而成:背景光穿过玻璃形成的透射、玻璃表面把环境反射回来的镜像、边缘处因为菲涅尔效应导致反射占比变高、以及最表面那层尖锐或模糊的高光。

离线渲染里,这些问题可以通过光线追踪逐步求解。但在 WebGPU、WebGL 这类实时渲染 API 里,没有一条光线真正穿过场景。它采用的是一种“近似合成”的思路:先把身后的背景渲染成一张纹理,再让玻璃物体的片断着色器去采样这张背景纹理,加一点 uv 偏移来模拟折射,再叠一层环境反射,最后通过混合状态合成到画面里。

所以玻璃材质的第一步,不是写 shader,而是决定“看到的背景从哪里来”。这个顺序如果错了,shader 写得再复杂也出不来效果。

1.2 一个效果目标,三种常见做法

不同项目里说的“玻璃材质”,可能完全是三件事。把它们混在一起,是很多方案变复杂的主要原因。

做法基本原理适合场景成本局限
平面半透明 / 玻璃拟态用 alpha blend 和低不透明度,叠一个模糊背景UI 卡片、后台面板、远处窗户没有折射,也没有真实反射
单次场景纹理折射先把不透明场景渲染到离屏纹理,玻璃片断对纹理做 uv 偏移玻璃杯、玻璃球、展示柜半透明物体之间不好处理,折射是屏幕空间近似
分层渲染 + 环境贴图反射背景层、玻璃层、高光层分离,反射用预滤波环境贴图产品展示、数字展厅、实时演示流程复杂,需要更多资源,需要排序策略

如果你只是想把一个后台面板做成磨砂玻璃质感,用第一种就够了。如果场景需要透过玻璃杯看到后面扭曲的背景,第二种是成本最低的可接受方案。第三种则是把玻璃效果变成工程能力时需要考虑的方向。

1.3 先做判断:你要的是“看起来像玻璃”还是“物体穿过去有折射”

这个判断听起来像废话,但很多项目真的会在开始前没想清楚。

如果玻璃在画面里只是背景装饰,没有明显的折射变形,直接用一个半透明材质叠加,再配一个高斯模糊背景,视觉上已经足够。如果玻璃后面有物体,你需要看到物体穿过玻璃之后产生偏移、扭曲,那就必须走“渲染到纹理 + 采样偏移”这条路。如果场景本身有复杂的环境反射,比如四周有发光灯带,那你还得决定反射来源是实时渲染,还是环境贴图。

我的建议是:先做最轻的方案,跑通之后再逐步加重。不要一开始就同时上折射、反射、多层排序和模糊,否则一旦效果不对,你根本不知道是哪一环出了问题。

2. WebGPU 的改变,不在“更漂亮”,而在“更可控”

2.1 管线状态的显式化:混合不再是上下文里的补丁

写过 WebGL 半透明效果的人,大概率都经历过这种场景:画完玻璃物体之后,忘了把混合模式从BLEND改回NONE,接下来画下一个不透明 UI 元素时,颜色干净得像是褪了层色。因为在 WebGL 里,混合模式、深度测试、面剔除都是全局状态,任何一个地方漏了复位,就会污染后续所有绘制。

WebGPU 把这套状态固化到了渲染管线对象里。你在创建RenderPipeline时就把混合模式、深度模板、面剔除方式一次性声明好。画玻璃时切换到玻璃管线,画不透明物体时切换到不透明管线,状态不会因为前一个物体“忘了关”而泄漏。

这对玻璃效果是非常关键的改进。玻璃不是一个固定的 blend 模式,它边缘的菲涅尔效果、反射层、透射层可能需要不同的合成方式。状态显式化之后,你可以更安全地在同一个 Render Pass 中切换不同管线,而不用担心某个全局开关被上一个物体改掉。

2.2 把场景渲染到纹理:这是折射采样的起点

WebGPU 里实现玻璃折射,核心依赖是“场景纹理”。也就是先把不透明的背景渲染到一张离屏纹理上,然后在玻璃材质片断着色器里采样这张纹理。

在 WebGL 中也能做类似操作,但 WebGPU 把纹理、纹理视图、绑定组、采样器这四者的关系定义得更清晰。你可以显式创建一张带 Mip 的纹理,也可以在 bind group 中只暴露某一层纹理视图;采样器是否使用线性过滤、是否使用寻址模式,都写在资源结构里。

这样做的好处是减少“资源状态”层面的错误。因为管线需要哪些纹理、哪些采样器、哪些 Buffer,在一个 Bind Group 里组织得明明白白。玻璃 shader 需要读场景纹理,就在创建 Bind Group 时绑进去;需要读环境贴图,就再加一个绑定;需要传 IOR 和折射强度等参数,就创建一个 uniform Buffer。

但这里也有一个必须养成的习惯:不要在同一个 Pass 里既写入某张纹理,又采样同一张纹理。后面会专门讲这个问题。

2.3 计算着色器给玻璃带来的不是效果,而是组织能力

WebGPU 相对 WebGL 的一个显著差异是支持计算着色器。计算着色器本身不会让玻璃“看起来更像玻璃”,但它让很多后处理步骤有了更合理的位置。

可以拿它做背景纹理的降采样模糊,磨砂玻璃的透射模糊不一定需要在片断着色器里多次采样,可以先在低分辨率纹理上做模糊,再传给玻璃 shader 使用;可以拿它做简单的屏幕空间折射后处理;可以拿它对多个透明层做加权累积,缓解半透明排序问题。

不过对入门阶段来说,计算着色器不是必须的。我更建议先用最朴素的流程跑通,等到你发现某个效果在片断着色器里做代价太高时,再把它搬到计算管线里。WebGPU 带来的不是“玻璃效果变强”,而是“你有了更多选择”。

2.4 别期待光线追踪:WebGPU 仍然在实时近似框架内

有一点需要提前确认:当前 WebGPU 规范没有内置光线追踪管线,也不存在“写了玻璃材质就自动获得真实折射”的能力。虽然相关扩展在讨论中,但作为应用开发者,不能把方案押在没有普及的能力上。

这意味着 WebGPU 里做玻璃,依然要依赖屏幕空间采样、菲涅尔近似、环境反射这些传统实时渲染手段。这不是姿态保守,而是现阶段最可靠的方式。理解了这一点,你就不会盯着“为什么玻璃边缘的折射不精确”不放,而是会把精力放在“如何在性能和效果之间取得平衡”这个真正重要的问题上。

3. 最小可运行流程:从背景纹理、混合状态到 WGSL 玻璃 shader

3.1 前置准备:画布、设备、管线与“渲染到纹理”目标

开始写代码前,需要先准备几样东西:

  1. WebGPU 设备实例,以及画布上下文。
  2. 一张离屏纹理,作为场景纹理,用于渲染不透明背景。
  3. 一套 Bind Group,把场景纹理和采样器暴露给玻璃 shader。
  4. 一个不透明物体管线和一个玻璃物体管线。

场景纹理的格式可以和画布一致,通常用RGBA8Unorm。如果你后续要做模糊和 HDR 效果,可能会需要RGBA16Float,但那是进阶选项。对小尺寸 demo 来说,保持一致格式最容易理解。

// 示意:创建离屏纹理作为场景渲染目标 const sceneTexture = device.createTexture({ size: [canvas.width, canvas.height], format: 'rgba8unorm', usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, });

这里的关键是使用权:这张纹理既要被渲染管线当作输出目标,又要被玻璃材质当作输入纹理采样,所以需要同时声明RENDER_ATTACHMENTTEXTURE_BINDING

3.2 帧结构:先渲染背景,再渲染玻璃,最后合成

一个最小可运行的帧结构大概是这样的:

  1. 创建 Render Pass,把sceneTexture作为颜色附件,清屏成背景色。
  2. 绘制所有不透明物体,比如背后的彩色平面、房间模型、灯光道具。
  3. 结束这个 Render Pass。
  4. 创建第二个 Render Pass,把最终画布作为颜色附件。
  5. 绑定刚才渲染完成的sceneTexture给玻璃 shader。
  6. 绘制玻璃物体,玻璃 shader 对sceneTexture做 uv 偏移采样,模拟折射。
  7. 结束第二个 Render Pass。

如果你只有一个透明物体,这个流程完全够用。如果有多个玻璃物体,就需要在第 6 步里按从远到近的顺序绘制,并让深度测试参与但不写入深度,或者使用其他透明排序策略。

注意:不要让玻璃物体的片断着色器既采样当前渲染目标,又把采样目标当作输出写入。WebGPU 中这种读写依赖会带来未定义行为,轻则部分平台黑屏,重则驱动报错。解决办法就是准备两张纹理:一张用于背景渲染,一张用于最终合成;或者把背景复制到一个独立采样纹理中。

3.3 配置混合状态:半透明的 alpha blend 怎么设

玻璃效果一定离不开混合。WebGPU 中,混合状态写在 Render Pipeline 的 fragment 目标配置里。下面是一个常见的半透明玻璃管线配置。

// 示意:半透明玻璃管线的混合配置 fragment: { module: shaderModule, entryPoint: 'fs', targets: [{ format: canvasFormat, blend: { color: { operation: 'add', srcFactor: 'one', dstFactor: 'one-minus-src-alpha' }, alpha: { operation: 'add', srcFactor: 'one', dstFactor: 'one-minus-src-alpha' } } }] }

这里使用的srcFactor: 'one',表示着色器输出的是预乘 Alpha 后的颜色。如果你写的是非预乘 Alpha,颜色分量没有提前乘以alpha,则颜色混合因子应该用src-alpha。两种写法不能混用。

玻璃效果最容易出现的“黑边”,往往就是这里搞错了:非预乘的透明物体会在边缘半透明区域暴露出偏暗的过渡色,因为颜色没有预乘,混合结果里混进了黑色背景。我建议在玻璃材质里统一用预乘 Alpha,这样边缘会更干净。

3.4 WGSL 玻璃 shader 骨架:透射、菲涅尔、反射

下面是一段用于理解流程的 WGSL 片断着色器骨架。它不追求物理正确,只展示玻璃材质的最小组成:透射采样、菲涅尔近似、反射叠加。

// 示意:WebGPU 玻璃材质片断着色器骨架 struct VSOutput { @builtin(position) position: vec4<f32>, @location(0) worldPos: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) uv: vec2<f32>, }; struct GlassParams { @align(16) refractionStrength: f32, @align(16) fresnelPower: f32, @align(16) reflectionStrength: f32, @align(16) alpha: f32, }; @group(0) @binding(0) var sceneTexture: texture_2d<f32>; @group(0) @binding(1) var sceneSampler: sampler; @group(0) @binding(2) var<uniform> params: GlassParams; @fragment fn fs(in: VSOutput) -> @location(0) vec4<f32> { let N = normalize(in.normal); let V = normalize(cameraPos - in.worldPos); let ndv = clamp(dot(N, V), 0.0, 1.0); // 简化折射:用视线与法线决定 uv 偏移 let refractedUV = in.uv + N.xy * params.refractionStrength; let transmissionColor = textureSample(sceneTexture, sceneSampler, refractedUV).rgb; // Schlick 菲涅尔近似 let f0 = vec3<f32>(0.04); let fresnel = f0 + (1.0 - f0) * pow(1.0 - ndv, params.fresnelPower); // 反射颜色:这里先用一个常数模拟,后续可换成环境贴图 let reflectColor = vec3<f32>(1.0, 1.0, 1.0) * params.reflectionStrength; let finalRGB = mix(transmissionColor, reflectColor, fresnel); let finalAlpha = params.alpha; return vec4<f32>(finalRGB * finalAlpha, finalAlpha); }

这段代码最核心的是refractedUV:它用物体表面法线的 xy 分量做了一个偏移。在真实玻璃里,折射方向由入射角、法线和材质 IOR 共同决定;这里的做法是屏幕空间近似,只适合作为最小流程,不要把它当成真实物理模型。

反射颜色暂时是一个常数。实际项目中,这里应该采样环境贴图,或者使用预滤波的反射探针。你可以在验证完透射和混合之后,再替换成环境贴图采样。

3.5 先跑通单物体,再谈排序

第一次实现时,建议在画面里放两个东西:一个规则格子的背景平面,一个位于画面中央的玻璃球。如果你能看到玻璃球区域背后的格子线发生偏移,说明“背景渲染到纹理 -> 玻璃 shader 采样纹理 -> 混合输出”这条链路已经通了。

这时候再调参数,效果是有确定反馈的。如果一上来就放一大堆半透明物体,遇到问题时你很难判断是折射没有生效,还是混合状态错误,还是 sorting 出了问题。

单物体跑通后,再尝试多个玻璃物体。此时必须处理半透明排序:先绘制不透明物体,再绘制所有透明物体;透明物体之间按相机距离从远到近绘制。否则只要摄像机稍微移动,玻璃的遮挡顺序就会变化,结果就是闪烁和穿插。

4. 参数、歧义和坑:为什么玻璃总是“脏、闪、黑”

4.1 从“结果不对”反推问题在哪一层

玻璃效果不对时,最忌讳的就是直接调 IOR 或者折射强度。因为很多问题不是参数问题,而是某个环节没有接好。

我一般会把问题按顺序分成四层:

  1. 输入层:场景纹理有没有正确渲染;Bind Group 有没有绑定;采样器有没有配置;法线和 UV 是否传到片断着色器。
  2. 管线层:混合状态是预乘还是非预乘;深度写入是否关闭;面剔除是否导致法线反向。
  3. 合成层:透明物体是否排序;背景是否清屏;输出纹理是否被后续步骤覆盖。
  4. 参数层:折射强度、菲涅尔、反射颜色、透明度是否合理。

先判断问题在哪一层,再动手改。

4.2 参数表:IOR、粗糙度、采样偏移、环境强度

这里给出一些常用参数范围,但你要知道,这些数值不是绝对标准,需要根据自己场景的分辨率、镜头距离和美术预期调整。

参数含义常用范围 / 建议注意事项
IOR折射率玻璃常见 1.45~1.6在屏幕空间近似里不能直接当作物理精确值
refractionStrengthuv 偏移强度0.02~0.15过大容易让背景纹理看起来“撕裂”
fresnelPower菲涅尔衰减曲线3~7越小边缘反射越强
reflectionStrength反射叠加强度0.1~0.5过高会变成镜面,失去透射感
roughness透射/反射模糊程度0.0~0.3需要配合 mip 采样或模糊纹理,否则没有意义
alpha透明度0.2~0.8必须与混合模式匹配,预乘与否会直接影响观感

4.3 典型现象与排查链路

几个常见的“玻璃翻车”现象,以及对应的排查方向:

  • 玻璃区域全黑:先查场景纹理有没有绑定;再查是否在同一张纹理上边采样边输出;最后查混合模式是不是把颜色全部乘掉了。
  • 背景只是平淡透出,没有折射:检查refractedUV偏移强度是否太小,以及法线是否在正确空间。如果法线方向反了,偏移方向也会反。
  • 玻璃边缘出现白边或黑边:检查 Alpha 是否预乘,混合因子是否匹配。白边通常是因为菲涅尔强度太高,黑边通常是因为非预乘 Alpha。
  • 多个玻璃物体穿插闪烁:透明排序问题。先解决从远到近的绘制顺序,再考虑更复杂的 OIT 方案。
  • 折射方向怪异,像把背景倒过来:检查 UV 坐标的坐标系是否一致。WebGPU 中纹理坐标原点和 WebGL 可能不同,需要统一约定。
  • 画面很亮,像镀了一层膜:反射强度或者菲涅尔系数过强,把透射颜色压下去了。

排查顺序建议固定为:看现象 -> 查输入纹理与 Bind Group -> 查混合状态 -> 查排序 -> 查参数边界。不要一上来就调 IOR,大概率是前面某层没接好。

5. 进阶路线:把玻璃材质从 Demo 变成工程能力

5.1 分层渲染:把玻璃拆成背景层、中层、高光层

当玻璃不再是一个测试球,而是要放进真实项目时,我会建议把玻璃效果拆成几个渲染层:

  • 背景层:不透明场景渲染到纹理。
  • 透射层:玻璃对背景纹理采样,做折射和透射。
  • 反射层:环境贴图反射叠加。
  • 高光层:镜面高光和边缘亮光,后处理或额外 Pass 产生。

拆层的好处是调参互不干扰。你可以单独调反射强度,而不用担心它会影响透射颜色;也可以在移动端直接去掉反射层,只保留透射和高光,完成降级。

5.2 反射来源:环境贴图比实时反射更适合 WebGL/WebGPU

很多人会把“反射”理解成实时渲染场景本身。但实际上,在 WebGPU 里做实时反射,成本很高。除非场景只有少数平面反射体,否则不建议让玻璃去反射场景中每一帧变化的物体。

更稳妥的方案是使用环境贴图。流程是这样:

  1. 加载一张 HDR 环境贴图。
  2. 在离线或加载阶段生成多级 Mip。
  3. 在玻璃 shader 中,根据 roughness 选择合适的 Mip 层级采样。
  4. 用菲涅尔系数在透射颜色和反射颜色之间做混合。

WebGPU 的采样器支持mipmapFilter,纹理也允许显式创建 Mip 链。这比 WebGL 里手动生成和绑定 Mip 纹理要清晰很多。如果你的项目里没有环境贴图资源,也可以先用渐变颜色或纯色模拟,但那样玻璃感会弱很多。

5.3 性能预算:分辨率、模糊次数、采样数与降级方案

玻璃效果的性能开销并不只在 shader 本身。离屏场景纹理的分辨率、模糊 Pass 次数、反射采样数量、环境贴图 Mip 选择,都会影响帧率。

配置项新手最小方案工程化方案
离屏纹理分辨率与画布相同画布的 0.5 倍或 0.75 倍
反射模糊不做降采样后一次模糊
折射采样每像素 1 次可配置开关
环境贴图 mip只用第一层按 roughness 选择层
透明物体排序1~2 个物体手动排多物体从远到近 + 分层

从工程经验看,性能问题最容易失控的地方是离屏纹理分辨率。很多人会把它设成和画布一样大,导致移动端帧率直接跳水。其实把玻璃背后的背景纹理降低到画布的 0.5 倍,肉眼很难察觉,尤其是做过 uv 偏移之后,分辨率损失会被折射模糊掩盖掉。

移动端还应该有一个总开关:当设备性能不好时,直接关闭折射,只保留半透明混合。这样视觉确实会有下降,但至少不会让页面卡顿到无法使用。

5.4 适用边界:哪些场景适合,哪些不适合

适合使用 WebGPU 玻璃材质的场景包括:

  • 产品展示:玻璃杯、香水瓶、灯具。
  • 数字展厅:需要在浏览器里实时查看物体材质。
  • 视觉实验:快速验证材质参数,不需要打包成原生应用。
  • 小型 3D 场景:物体数量不多,透明物体有限。

不适合的情况也很明显:

  • 场景里有大量玻璃物体并相互穿插,这需要 OIT 或真正的光追,不是普通 WebGPU 实时管线能轻松解决的。
  • 需要兼容老旧浏览器或 WebGL 1.0 时代的设备。
  • 需要完全物理正确的折射和色散,而不是视觉近似。
  • 对性能极度敏感的移动端首屏,比如电商首页里的 3D 小挂件。

6. 我的最终判断:这项能力真正改变的是什么

6.1 玻璃材质最终考验的是流程管理

从 WebGL 到 WebGPU,玻璃材质最核心的变化,不是从“难看”变成“好看”,而是从“靠临时修改全局状态凑出来”变成“可以作为工程能力被沉淀”。管线显式、混合模式可配置、纹理资源关系清晰、计算着色器留出了后处理空间,意味着你可以把玻璃材质封装成项目内可复用的模块。

回头看每一个环节:背景纹理怎么渲染、玻璃 shader 怎么采样、混合状态怎么配、透明物体怎么排序、性能怎么降级。这些都不是某个 shader 技巧能解决的,而是完整的渲染流程设计问题。能把这个流程管好,WebGPU 里的玻璃材质会比 WebGL 时代可控得多。

6.2 下一步建议

如果你打算开始做 WebGPU 玻璃材质,我建议不要直接尝试复杂的成品场景。先用一个背景格子平面加一个玻璃球,跑通“渲染背景到纹理 -> 玻璃 shader 采样纹理 -> 混合输出”三层流程。然后加上菲涅尔和环境反射,感受边缘变化。再尝试多个玻璃物体,理解排序带来的问题。最后再想工程化降级。

玻璃材质看起来是一个视觉技巧,实际上是在检验一个人是否真正理解和组织渲染流程。它需要的不只是“知道 API”,而是理解每一帧里,颜色从哪儿来、经过什么变换、最后怎么落到屏幕上。这条路没有捷径,但一旦跑通一次,你会发现 WebGPU 把这件事变得比过去清晰太多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询